iOS底層原理之方法呼叫的底層探究

語言: CN / TW / HK

本文主要內容

1、objc_msgSendSuper解析 \ 2、方法的快速查詢 \ 3、方法的慢速查詢演算法 \ 4、方法的慢速查詢流程 \ 5、總結

前言

iOS底層原理之cache詳解文章中,我們瞭解到了cache,知道了insert操作,那麼既然有儲存的操作,就會有取的操作.在objc4-838.1原始碼的cache原始碼中(62行)發現,編譯器從cache中呼叫方法要用到objc_msgsendcache_getImp,如下圖。訊息傳送就是runtime通過sel找到imp的過程,訊息傳送在編譯階段編譯器會把這個過程轉換成objc_msgSend函式。因此,接下來我們來詳細瞭解objc_msgSend這個函式。 image.png 擴充套件知識點 OC:為一門動態的語言,動態語言指程式在執行過程中,可以對類、變數進行修改,可以改變其資料結構, 比如可以新增或刪除函式,可以改變變數的值等。在編譯階段並不知道變數的資料型別,也不清楚要呼叫哪些函式, 只有在執行時才去檢查變數的資料型別,根據函式名找到函式的實現! C:為一門靜態的語言,在編譯階段就確定了所有變數的資料型別,同時也確定了要呼叫的函式,不能進行動態的修改! runtime:就是可以實現語言動態的API。runtime的2個核心:一是類的各方面的動態配置; 二是訊息傳遞(訊息傳送+訊息轉發)。

一.objc_msgSendSuper解析

1、objc_msgSend初探

在之前我們經常談到方法呼叫就是訊息的傳送,其本質就是objc_msgSend傳送訊息。首先我們新建一個工程,new一個LSPerson類,在這個類裡面實現下面兩個方法,程式碼如下:

```OC @interface LSPerson : NSObject

  • (void)study;

  • (void)happy;

@end

// 在main方法呼叫 LSPerson *person = [LSPerson new]; [person study]; [person happy]; `` 然後我們命令列開啟main.m檔案所在位置,執行clang -rewrite-objc main.m,編譯main.m, 在同目錄下得到main.cpp`, 開啟這個檔案找到main方法,我們會找到如下程式碼:

OC // 對應的LSPerson *person = [LSPerson new]; LSPerson *person = ((LSPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LSPerson"), sel_registerName("new")); // 對應的 [person study] ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("study // 對應的 [person happy] ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("happy")); 通過上面的編譯的程式碼我們知道,呼叫方法實際最終就是呼叫objc_msgSend函式傳送訊息,objc_msgSend函式預設有兩個引數,第一個引數訊息接受者,第二個引數就是訊息的方法名(sel), 如果方法有引數,就接著後面第三個、第四個等引數。objc_msgSend會根據訊息接受者和訊息的方法名找到訊息的方法實現(imp)。如果訊息的接受者是例項物件,那麼就到例項物件的isa指標(類物件)中根據sel找到方法的實現(imp);如果訊息的接受者是類物件,那麼就到類物件的isa指標(元類)中根據sel找到方法的實現(imp);\ 這裡有一個疑問,系統幫我們編譯成了objc_msgSend函式,那麼我們能否在程式碼中直接使用objc_msgSend函數了?答案是可以的。我們將編譯後的程式碼,直接替換上面的程式碼在code裡面執行如下

``` // #import 需要引入系統的類庫 LSPerson person = [LSPerson new]; ((void ()(id, SEL))(void )objc_msgSend)((id)person, sel_registerName("study")); ((void ()(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("happy"));

//輸出結果 // [LSPerson study] // [LSPerson happy] ``` 在上面編譯的main.cpp裡面我們發現,不僅有objc_msgSend函式,還有objc_msgSendSuper等函式具體如下:

void objc_msgSend(void): 傳送訊息 \ void objc_msgSendSuper(void): 發訊息給物件的父類時會編譯成此函式 \ void objc_msgSend_stret(void): 發訊息的返回值為結構體時會編譯成此函式 \

void objc_msgSendSuper_stret(void): 發訊息給物件的父類的返回值為結構體時會編譯成此函式\ void objc_msgSend_fpret(void): 發訊息的返回值為浮點型時會編譯成此函式 接下來我們看繼續研究objc_msgSendSuper函式。

2、objc_msgSendSuper函式

新增加一個類LSTeacher繼承自LSPerson,在LSTeacher類裡面重寫init方法,增加NSlog方法,程式碼如下:

```OC // @implementation LSTeacher - (instancetype)init { if (self = [super init]) { NSLog(@"%@", [self class]); NSLog(@"%@", [super class]); } return self; }

  • (void)study { [super study] }

// mian.m LSTeacher *teacher = [LSTeacher new]; [teacher study]; // 輸出結果 // LSTeacher // LSTeacher // [LSPerson study] `` 從輸出結果[self class] 輸出LSTeacher,[super class]輸出LSTeacher, [teacher study]輸出的是 [LSPerson study];這裡就有疑問了為什麼 [super class]輸出的是LSTeacher?.我們將這個檔案編譯成Cpp看一下,執行clang -rewrite-objc LSTeacher.m`得到LSTeacher.cpp檔案,檢視原始碼如下:

```OC if (self = ((LSTeacher ()(__rw_objc_super , SEL))(void )objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LSTeacher"))}, sel_registerName("init"))) { NSLog((NSString )&__NSConstantStringImpl__var_folders_xk_b2kwsh915s5bxyyjvtqt6dg40000gn_T_LSTeacher_4d907e_mi_0, ((Class ()(id, SEL))(void )objc_msgSend)((id)self, sel_registerName("class"))); NSLog((NSString )&__NSConstantStringImpl__var_folders_xk_b2kwsh915s5bxyyjvtqt6dg40000gn_T_LSTeacher_4d907e_mi_1, ((Class ()(__rw_objc_super , SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LSTeacher"))}, sel_registerName("class"))); }

```

[self class]: 在編譯時會轉化為objc_msgSend函式,所以輸出LSTeacher \ [super class]: 在編譯時轉化為objc_msgSendSuper函式,而該函式和objc_msgSend函式的唯一區別就是在找方法的時候,objc_msgSendSuper是先從父類中找方法的實現。同理[super study]打印出來的結果是先去父類中尋找該方法的實現,所以打印出[LSPerson study];

為了更好的理解objc_msgSendSuper函式的呼叫機制,我們手動實現super關鍵字,建立型別objc_super的結構體,先檢視objc_super的型別定義如下 image.png 手動實現super關鍵字程式碼如下

```OC // #import - (void)study { NSLog(@"%s", func); struct objc_super ls_objc_super; ls_objc_super.receiver = self; ls_objc_super.super_class = LSPerson.class;

void* (*objc_msgSendSuperTyped)(struct objc_super *self, SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&ls_objc_super, @selector(study));

}

// 輸出結果 // [LSTeacher study] ```

解釋: 1、當super_class 為父類時,就會去父類裡面去查詢方法 2、當把super_class 改成 LSTeacher.class 這個時候會造成無限迴圈呼叫該方法 3、當把super_class 改成 NSObject.class 這個時候程式會crash,因為NSObject裡面並沒有該方法的實現。 總結: objc_msgSendSuper函式objc_msgSend的函式唯一區別在於找方法的時候出發點不同,也就是、objc_msgSendSuper函式是從父類中找方法的實現

二、方法的快速查詢

在上面我們知道了objc_msgSend函式,我們經常聽說呼叫方法的時候會有一個快速查詢的流程,我們結合objc4-838.1原始碼進行除錯,在原始碼中搜索objc_msgSend檢視函式實現,這裡我們以arm64實現為主。程式碼部分截圖如下 image.png 通過上面的程式碼我們知道objc_msgSend底層是用匯編實現的。

擴充套件知識點\ 1、為什麼objc_msgSend底層使用匯編? 主要原因是彙編比C快,同時彙編會免去大量對區域性變數的拷貝作用,引數被直接存放在暫存器中。\ 2、彙編會在函式和全域性變數前加一個下劃線_ 在程式中往往會包含彙編和C檔案,對於編譯器來說兩者是一樣的,因此可能會出現問題,為了防止符號名的衝突在彙編中函式和全域性變數前會加一個下劃線啊_(如上面程式碼截圖裡面) 接下來我們在我們上面建立的工程裡面增加斷點通過彙編的方式來配合原始碼進行檢視objc_msgSend的方法快速查詢流程。

image.png 在上圖的位置增加斷點,當斷點執行到這個地方的時候,開始彙編除錯(Debug -> Debug Workflow -> Always show Disassembly) image.png 按住Control,點選Step into單步除錯 image.png 點選Step,進入objc_msgSend底層實現. \ 第1步 配合原始碼我們知道先判斷p0(訊息接受者)是否存在,不存在則重新開始執行objc_msgSend。通過讀取暫存器證明此時的x0確實是LSTeacher物件. image.png 第2步 通過p13讀取isa(下圖1為objc原始碼objc_msgSend底層函式實現),圖二里面x13就是LSTeacher例項物件isa的地址 image.png image.png 第3步 第二步取到物件isa地址之後,再通過isa取類物件,並儲存到x16暫存器中,驗證x16暫存器地址和LSTeacher類物件的地址相同,程式碼截圖如下: image.png 第4步 從x16中取出類物件移到x15中,通過x16記憶體平移得到cache 圖1是objc原始碼objc_msgSend函式實現。 image.png image.png 第5步 通過cache找儲存方法的buckets。通過iOS底層原理之cache詳解中通過原始碼分析cache的快取內容的方法除錯查詢。 image.png 第6步 如果在cache中找到buckets,在buckets中找到對應的sel,就會呼叫cacheHit;如果沒有找到,就會呼叫objc_msgSend_uncached函式。 image.png image.png 總結:方法(例項方法和類方法)的快速查詢\ objc_msgSend(receiver, sel, ...(其它方法的引數))\ 1. 判斷 receiver是否存在; 2. 通過receiverisa指標找到對應的class; 3. class記憶體平移找到cache; 4. 通過cache找到儲存方法的buckets; 5. 遍歷buckets看快取中是否存在sel方法; 6. 如果buckets中快取有sel方法,會呼叫cacheHit(快取命中),然後會呼叫imp; 7. 如果buckets中快取沒有sel方法,會呼叫objc_msgSend_uncached函式;

三、方法的慢速查詢演算法

在方法的快速查詢中,如果buckets中快取沒有self方法,會呼叫objc_msgSend_uncached函式。本小節來分析此函式。找到objc_msgSend_uncached函式原始碼,執行MethodTableLookup,跳轉lookUpImpOrForward函式。 image.png image.png lookUpImpOrForward函式實現中,如下部分是系統為呼叫此函式做的準備工作。 image.png 接著,再一次從cache裡面取找imp,這是為了防止多執行緒操作時剛好呼叫方法,此時快取進來了。如果在cache還是找不到,就去方法列表中查詢。 image.png 類的方法列表中查詢具體查詢程式碼如下: image.png image.png 使用二分法快速查詢方法,結果可能找到也可能找不到,程式碼如下圖 image.png 總結:當方法在cache裡面找不到的時候會進入objc_msgSend_uncached函式,在這個函式裡面會再次去cache裡面找到(防止多執行緒問題),然後再去類物件方法列表中查詢,查詢主要是方法列表的迴圈(迴圈主要是採用二分法演算法).

四、方法的慢速查詢流程

上一小節中如果從方法列表中找到,執行done,呼叫log_and_fill_cache函式,這個函式中實現了將方法插入到快取中。 image.png image.png 如果方法列表中沒有找到,就會將當前的類賦為父類去父類中找,先到父類的快取中找,找不到就會再次進入for (unsigned attempts = unreasonableClassCount() ;;)迴圈到父類的方法列表中查詢。 image.png 如果在父類的方法列表也沒找到就到父類的父類查詢,直到父類為nil,如果還沒找到,就呼叫forward_imp(方法轉發)。 總結:方法(例項方法和類方法)的慢速查詢\ lookUpImpOrForward函式\ 1. 先在當前類的methodList中查詢,如果找到會進行快取; 2. 在當前類的methodList中沒找到,去父類的cache中查詢; 3. 在父類的cache中沒找到就會到父類的methodList中查詢; 4. 直到父類為nil,如果還沒找到,就會呼叫forward_imp,即訊息的轉發

五、總結

  1. OC裡面物件呼叫方法實際上底層實現是objc_msgSend、objc_msgSendSuper等函式。該函式接受兩個最少兩個引數,reciver+sel,第一個引數表示訊息接受者,通過該引數去找isa,找cache,找方法列表。第二個引數是方法名。
  2. 在方法的快速查詢就是從cache中查詢,而cache的底層實現是用的彙編, 彙編相對更快。
  3. 在方法的慢速查詢中,會再次從cache中找到(主要解決多執行緒操作的時候,cache裡面可能插入了這個方法),然後再去類物件(通過reciver的isa指標找到類物件)方法列表中查詢,這個查詢方法主要是對方法列表的迴圈(採用的二分查詢演算法),找到則返回。如果沒有找到,就去父類查詢,將父類賦值給當前類,重複上述動作,直到父類不存在,最終找到imp則存入快取中,如果父類沒有找到則進入到forward_imp(即訊息轉發)。

有任何問題,歡迎大家評指出哦!覺得寫的不錯的,麻煩點個贊哦