漫談 objc_msgSend

你肯定聽過 oc 的本質就是發(fā)消息這句話, 這篇文章就來介紹一下 objc_msgSend 是什么以及它是如何工作的。

OC 發(fā)消息機制

Objective-C 的底層實現(xiàn)是 C, C 語言使用的是靜態(tài)綁定, 也就是說, 當程序在編譯期就知道了程序在運行期所調用的函數(shù), 而 OC 則是動態(tài)綁定, 也就是當程序在運行期的時候才能知道具體調用什么方法。
  我們常常會寫到[object messageName:parameter], 其中 object 叫做"接收者", messageName 叫做"選擇子", parameter 為參數(shù), 選擇子和參數(shù)結合起來成為"消息"。
  編譯期看到此消息后, 會將其轉換為 objc_msgSend 來調用, 這個函數(shù)會根據(jù)接受者和選擇子的類型來尋找并調用合適的方法, 在每個對象所屬的類中, 有一個"方法列表(objc_method_list)", 里面存了該類所有的"方法(objc_method)", 每個"方法"里包含 "方法名稱(SEL)" 和 "函數(shù)指針(IMP)", "函數(shù)指針"指向該方法的地址, 在每個對象所屬的類里面還有一個"快速映射表(objc_cache)", 當 object 發(fā)消息并且該消息沒有在"快速映射表"里時候, 會先判斷本類是否有與選擇子名稱相同的方法, 如果有會將該方法緩存起來并執(zhí)行, 當后續(xù)還使用該類調用了同樣的方法時, 會先去快速映射表里尋找

什么是 objc_msgSend?

objc_msgSend 的原型為:

void objc_msgSend(id self, SEL cmd, ...)

這是個"參數(shù)個數(shù)可變的函數(shù)", 可以接收兩個或兩個以上的參數(shù)。第一個參數(shù)為接收者, 第二個參數(shù)為選擇子, 后續(xù)參數(shù)為消息中的一些參數(shù)。
  編譯期會把剛剛上述的[object messageName:parameter]轉換為下面這樣的函數(shù):

objc_msgSend(object, @selector(messageName:), parameter);

然后 objc_msgSend 函數(shù)會根據(jù)接收者和選擇子的類型來調用適當?shù)姆椒ā?/p>

下面為上述objc_method_listobjc_cache的結構
具體代碼在蘋果官方開源網(wǎng)站上: objc-runtime.h

struct objc_method_list {
#if defined(Release3CompatibilityBuild)
        struct objc_method_list *method_next;
#else
    struct objc_method_list *obsolete;
#endif

    int method_count;
#ifdef __alpha__
    int space;
#endif
    struct objc_method {
        SEL method_name;
        char *method_types;
        IMP method_imp;
    } method_list[1];       /* variable length structure */
};

typedef struct objc_method *Method;

struct objc_cache {
    unsigned int mask;            /* total = mask + 1 */
    unsigned int occupied;        
    Method buckets[1];
};

objc_method_list 是用于儲存 objc_method 的數(shù)組列表
objc_cache 是用于緩存調用過的 method
Methodobjc_method 結構體的指針

消息派發(fā)(消息傳遞)

上述說到 objc_msgSend 函數(shù)會根據(jù)接收者和選擇子的類型來調用適當?shù)姆椒? 操作流程如下:

  • 該方法會先在接收者所屬的類 objc_method_list 中搜索, 如果能找到選擇子與方法名稱相同的方法則執(zhí)行
  • 如果找不到, 就會沿著繼承體系向上尋找, 如果找到則執(zhí)行
  • 如果最終還是沒有找到與之對應的方法, 那么將會觸發(fā)"消息轉發(fā)"的機制

消息轉發(fā)

當接收者所屬的類與向上的繼承體系都沒有找到該方法的時候, 會出發(fā)消息轉發(fā), 我們一定遇到過這樣的問題

-[__NSCFConstantString setTitle:forState:]: unrecognized selector sent to instance 0x100fb0200

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 

reason: '-[__NSCFConstantString setTitle:forState:]: unrecognized selector sent to instance 0x100fb0200'

這段異常信息是有 NSObject 的 doesNotRecognizeSelector 所拋出的, 就是 __NSCFConstantString 無法處理 setTitle:forState: 這個方法, 在經(jīng)過了一系列的消息轉發(fā)后由 NSInvocation把這條消息所有的信息封裝起來, 調用了 NSObject 類的方法 doesNotRecognizeSelector 拋出異常。

消息轉發(fā)分為三步:

  • 動態(tài)方法解析

    對象在收到無法處理的消息時, 會先調用所屬類的 + (BOOL)resolveInstanceMethod:(SEL)sel 方法, 這個方法會判斷這個類能否新增一個實例方法來處理這個選擇子, 如果是收到的是類方法, 那么會調用 + (BOOL)resolveClassMethod:(SEL)sel 來判斷。
      上述方法執(zhí)行的條件必須開發(fā)者已經(jīng)寫好了具體實現(xiàn)代碼, 只是讓系統(tǒng)在運行期動態(tài)新增方法

  • 備援接收者

    如果第一步無法處理這條消息, 那么在備援接收者這一步, 會調用 - (id)forwardingTargetForSelector:(SEL)aSelector 來尋找接收者是否有備援接收者, 如果有則返回該對象, 然后由這個對象去處理這條消息, 沒有的話則返回 nil, 返回 nil 說明這條消息沒有備援接收者, 然后會啟用完整的消息轉發(fā)。

  • 完整的消息轉發(fā)

    如果轉發(fā)算法已經(jīng)到了這里, 那么就要啟用完整的消息轉發(fā)機制了。首先系統(tǒng)會創(chuàng)建 NSInvocation 對象, 把那條消息有關的全部信息封裝到這個對象中, 然后"消息派發(fā)系統(tǒng)"會調用
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    把消息指派給目標對象。
      實現(xiàn)此方法時, 會先判斷本類是否可以處理此方法, 如果不可以, 則會向上調用父類的方法, 直到 NSObject。 如果最后調到了NSObject 類的方法時, 仍未處理這條消息, 那么會調用 doesNotRecognizeSelector 方法來拋出異常。

總結

  • [object messageName:parameter] 會先在編譯期轉為 objc_msgSend(object, @selector(messageName:), parameter)
  • 在運行期根據(jù)對象所在類下的 objc_method_listobjc_cache 尋找可執(zhí)行的方法并執(zhí)行
  • 如果找不到會沿繼承體系向上查找可執(zhí)行方法來執(zhí)行
  • 如果仍然找不到會觸發(fā)消息轉發(fā)機制, 判斷這個類能否新增一個實例方法來處理這個選擇子
  • 無法處理則尋找接收者是否有備援接收者, 并由備援接收者處理消息
  • 創(chuàng)建并封裝 NSInvocation 對象, "消息派發(fā)系統(tǒng)"把消息指派給目標對象, 一直向上調用父類方法, 到 NSObject 后仍未處理, 則拋出異常
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容