與你分享一份面試題關於iOS底層原理

語言: CN / TW / HK

highlight: atelier-sulphurpool-light

這是我參與8月更文挑戰的第5天,活動詳情檢視:8月更文挑戰

js 寫在前面: iOS底層原理探索的階段總結是對於底層探索的總結內容, 篇章不會太多,主要是對於探索中的細節總結。 希望對大家能有幫助。


總結來自以下專欄內容



load 方法在什麼時候呼叫

load_images 分析

load_images 的時候 , 在 add_classs_to_loadable_list 中進行單個類的收集在loadable_classes 表中,在 add_category_to_loadable_list 中進行分類的收集在 loadable_categories表中 ,然後統一的 ( call_class_loads call_category_loads ) 進行遞迴將 load 方法進行呼叫。 load方法的呼叫順序為父類、子類、分類。

load_images 方法中 會先 發現 load 方法 通過 prepare_load_methods((const headerType *)mh);, 之後是對load方法進行呼叫 call_load_methods(); ;

```js void load_images(const char path __unused, const struct mach_header mh) { if (!didInitialAttachCategories && didCallDyldNotifyRegister) { didInitialAttachCategories = true; loadAllCategories(); }

// 如果這裡沒有+load方法,則返回而不獲取鎖。
if (!hasLoadMethods((const headerType *)mh)) return;

recursive_mutex_locker_t lock(loadMethodLock);

// 發現 load 方法
{
    mutex_locker_t lock2(runtimeLock);
    prepare_load_methods((const headerType *)mh);
    // 遞迴自己到父類一直到nil到load方法

// 加到表裡 add_class_to_loadable_list(cls); 讀取類的方法 匹配是 load 後新增 ( cls 和 imp )key-value的形式         // 做一個標記 cls->setInfo(RW_LOADED); // schedule_class_load(remapClass(classlist[i])); }

// 呼叫 +load 方法
call_load_methods();
// do{} while() 迴圈 來呼叫

} ```

此時,又有一個問題 , load 方法和 initialize 方法誰先呼叫? load 方法

initialize 方法是在第一次訊息的時候呼叫,也就是 lookUpImpOrForward 的時候呼叫。 - 分類的⽅法是在類realize之後attach進去的插在前⾯,所以如果分類中實現了initialize方法,會優先調⽤分類的initialize方法。 - initialize內部實現原理是訊息傳送,所以如果子類沒有實現initialize會呼叫父類的initialize方法,並且會呼叫兩次 - 因為內部同時使用了遞迴,所以如果子類和父類都實現了initialize方法,那麼會優先呼叫父類的,在呼叫子類的

此處 load 方法, initialize 方法 都可以自起,還有一個方法可以自起 —— c++ 的建構函式 方法。

那麼,這些方法的一個先後順序是怎樣的呢? - 在分析dyld之後,可以確定這樣的一個呼叫順序,load->c++->main函式 - 寫在objc工程中的c++方法 在objc_init()呼叫時,會通過static_init()方法優先呼叫c++函式,而不需要等到_dyld_objc_notify_registerdyld註冊load_images之後再呼叫 - 同時,如果objc_init()自啟的話也不需要dyld進行啟動,也可能會發生c++函式在load方法之前呼叫的情況

### 補充 專案中 類和分類的 載入 順序 是什麼?

image.png 通過這裡來調整編譯順序

Runtime是什麼

  • Runtime是由CC++彙編實現的⼀套API,為OC語⾔加⼊了⾯向物件,運⾏時的功能

  • 運⾏時(Runtime)是指將資料型別的確定由編譯時推遲到了運⾏時,如類擴充套件和分類的區別

  • 平時編寫的OC程式碼,在程式運⾏過程中,其實最終會轉換成RuntimeC語⾔程式碼,Runtime 是 Object-C 的幕後⼯作者

⽅法的本質,sel是什麼?IMP是什麼?兩者之間的關係⼜是什麼?

  • ⽅法的本質:傳送訊息,訊息會有以下⼏個流程:

    1. 快速查詢 (objc_msgSend)~ cache_t 快取訊息
    2. 慢速查詢~ 遞迴⾃⼰或⽗類 ~ lookUpImpOrForward
    3. 查詢不到訊息: 動態⽅法解析 ~ resolveInstanceMethod
    4. 訊息快速轉發 ~ forwardingTargetForSelector
    5. 訊息慢速轉發 ~ methodSignatureForSelectorforwardInvocation
  • sel是⽅法編號,在read_images期間就編譯進⼊了記憶體

  • imp就是我們函式實現指標,找imp就是找函式的過程

  • sel就相當於書本的⽬錄tittle

  • imp就是書本的⻚碼

  • 查詢具體的函式就是想看這本書⾥⾯具體篇章的內容

    1. 我們⾸先知道想看什麼 ~ tittle (sel)
    2. 根據⽬錄對應的⻚碼 (imp
    3. 翻到具體的內容 方法實現

能否向編譯後的得到的類中增加例項變數? 能否想執行時建立的類中新增例項變數

1:不能向編譯後的得到的類中增加例項變數 - 我們編譯好的例項變數儲存的位置在 ro,一旦編譯完成,記憶體結構就完全確定 就無法修改 - 可以通過分類向類中新增方法和屬性(關聯物件)

2:只要內沒有註冊到記憶體還是可以新增

  • 可以新增屬性 + 方法

可以通過objc_allocateClassPair在執行時建立類,並向其中新增成員變數和屬性,見下面程式碼:

```js // 使用objc_allocateClassPair建立一個類Class const char * className = "SelClass"; Class SelfClass = objc_getClass(className); if (!SelfClass){ Class superClass = [NSObject class]; SelfClass = objc_allocateClassPair(superClass, className, 0); }

// 使用class_addIvar新增一個成員變數 BOOL isSuccess = class_addIvar(SelfClass, "name", sizeof(NSString ), log2(_Alignof(NSString )), @encode(NSString *));

class_addMethod(SelfClass, @selector(addMethodForMyClass:), (IMP)addMethodForMyClass, "[email protected]:");

```

[self class][super class]的區別以及原理分析

我們新建一個繼承自 SMPerson 的 SMTeacher 類

```js @implementation SMTeacher

  • (instancetype)init {

    self = [super init]; if (self) {

    //SMTeacher - SMPerson
    NSLog(@"%@ - %@", [self class], [super class] );
    

    }

    return self; }

@end 執行專案後列印js SMTeacher - SMTeacher ```

和我們的猜想在super這一部分的列印是不一樣的, 接下來我們就需要分析下原因了:

首先 class 實現如下: ```js - (Class)class {     return object_getClass(self); }

Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } `` 方法中有兩個隱藏引數 :- (Class)class( id self, SEL _cmd ) { },此方法在OC底層都是通過objc_msgSend(id self, SEL _cmd)` 方法來發送訊息。

[self class]

所以第一個引數是方法的呼叫者 SMTeacher 例項class方法 通過isa找到例項變數的類也就是 SMTeacher 類

[super class]

super 和 self 不一樣的點首先是 super 是一個編譯期的關鍵字,並不是引數名。

通過斷點除錯資訊可以看到,實際上是呼叫了 objc_msgSendSuper2

image.png

js objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

super 底層是一個結構體 ```js struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver;

/// Specifies the particular superclass of the instance to message. 
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */

}; ``` 可以看到 receiver 依然是 SMTeacher 例項, 只不過 會先去呼叫SMTeacher的父類中的方法。

所以,其本質是 objc_msgSendSuper,訊息的接受者還是 self ,方法編號: class。 只是 objc_msgSenderSuper 會更快,直接跳過self的查詢。

補充

通過clang SMTeacher.h 檔案 可以看到 super 是呼叫的 objc_msgSendSuper

image.png

檢視objc_msgSendSuper內部實現,最終呼叫到了objc_msgSendSuper2

image.png

記憶體平移

準備

類的記憶體結構

案例程式碼: ```js Person1 *p = [Person1 alloc]; [p sleep];

Class cls = [Person1 class];
void *sm = &cls;
[(__bridge id)sm sleep];

js - (void)sleep {

NSLog(@"person - %s", __func__);

} 列印結果:js person - -[Person sleep] person - -[Person sleep] ```

顯然 對於 例項p 執行 sleep 方法 列印內容,我們都能理解,那麼,指標sm 為什麼也可以 執行 sleep 方法 列印內容呢?

這樣就要看一下,例項物件執行方法的一個過程: 1. 例項物件的 isa 2. 類的首地址
3. 通過記憶體平移找到類的 bits 4. rw 指向 ro 5. 找到 methods 這樣就可以在方法列表中找到方法的地址,然後去執行。

這樣的一個流程下來,對於 指標 *sm 來說,它也是指向 Person類 的 , 所以按照上面的流程 同樣可以找到方法,然後去執行。

  • 現在對 sleep 稍作修改: ```js
  • (void)sleep {

    NSLog(@"person - %s -%@", func , self.name); } ``` 執行後列印內容:

image.png

我們對比一下 例項p 和 指標sm,他們很明顯是不一樣的, 我們先 xcrun 一下 看看底層原始碼:

_I_Person1_name

image.png

OBJC_IVAR_$_Person1$_name

image.png

  • 所以在方法中列印name, 我們能獲取到是通過 記憶體的偏移 來獲取到的。

image.png

記憶體地址偏移 0x8 在viewDidLoad 的棧幀中列印內容,接下來我們探索下 viewDidLoad這個方法的棧幀( 到底哪些內容壓棧了 ):

viewDidLoad 中哪些內容壓棧了

  • 棧記憶體地址: 高 -> 低
  • 堆記憶體地址: 低 -> 高

首先在 Person1 中 再新增一個屬性: ```js @interface Person1 : NSObject

@property (nonatomic, retain) NSString like; @property (nonatomic, retain) NSString name;

  • (void)sleep;

@end ```

在此執行專案,檢視列印的內容: js person - -[Person1 sleep] -SuperMan person - -[Person1 sleep] -<ViewController: 0x7ff0b1504a60> 這次很奇怪 是列印了 ViewController

這次因為添加了個屬性,所以這次的 Person1 列印name的時候需要偏移0x10;

  • 引數壓棧
  • 結構體壓棧

驗證:

  • 自定義需要引數的方法

image.png

引數從前往後壓棧
  • 自定義一個結構體 js struct SMPer {     NSNumber *num1;     NSNumber *num2; }; 列印內容 js person - -[Person1 sleep] -SuperMan person - -[Person1 sleep] -50 明顯看到了我們定義的結構體的地址是在p和cls之間,(結構體是倒著壓棧的)

image.png

結構體是從後往前壓棧的

最後,我們看下整個 viewDidLoad 方法的壓棧情況: 還記得最開始我們 xcrun 了 ViewController.m 我們就在這個檔案中找到 viewDidLoad 的方法,來分析:

```js struct __rw_objc_super { struct objc_object object; struct objc_object superClass; __rw_objc_super(struct objc_object o, struct objc_object s) : object(o), superClass(s) {} };

...

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {

((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){

    (id)self,
    (id)class_getSuperclass(objc_getClass("ViewController"))

}
  , sel_registerName("viewDidLoad"));



Person1 *p = ((Person1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person1"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sleep"));

Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person1"), sel_registerName("class"));
void *sm = &cls;
((void (*)(id, SEL))(void *)objc_msgSend)((id)sm, sel_registerName("sleep"));

}

``` xcrun還原底層程式碼後,看起來就舒服多了,分總結如下:

viewDidLoad 壓棧分析.001.jpeg