重拾 ObjC 訊息機制

Smallfly發表於2019-03-03

訊息機制是 Objective-C 語言的基礎,也是它動態化的核心所在。筆者在閱讀 objc 原始碼之後,對該語言的使用有了一些新的思考。

物件或者類在響應訊息時,最多會經歷 5 個過程:

  1. 查詢當前類的快取
  2. 在當前類的方法列表查詢
  3. 在父類快取及方法列表查詢
  4. 訊息決議
  5. 訊息轉發

訊息的響應過程其實是根據選擇子(sel)查詢對應的函式實現(imp)的過程。

查詢當前類的快取

傳送訊息的objc_msgSend函式會通過cache_getImp函式查詢快取,因為所有方法的執行,都會呼叫這兩個函式,為了提高效能,這兩個函式由彙編實現。

如果方法快取沒有命中,會在當前類的methods中查詢。

查詢當前類的方法列表

當前類的方法列表底層是一個二維method_list_t陣列,儲存當前類以及所有 category 的方法。二維的方法列表是在 runtime 初始化的時候構建的,先新增主類的方法列表,然後依次新增 category 的,後新增的列表會放在陣列的前面。

在查詢時,會依次遍歷二維陣列中的每個元素列表,然後在列表中使用二分查詢,直到找到選擇子(sel)對應的方法實現(imp)才終止。找到後使用log_and_fill_cache寫入當前類的快取。

如果這個過程沒有找到,會進入父類查詢。

在父類快取及方法列表查詢

在查詢父類的方法列表前,會先查詢父類的方法快取,如果快取沒有命中才會遍歷方法列表查詢。查詢過程和在當前類查詢有點區別是,如果在快取中找到訊息轉發的 imp _objc_msgForward_impcache,會停止查詢,直接進入訊息轉發。

如果在父類中找到對應的實現,會將該方法快取到當前類中。

如果直到 NSObject 都沒有找到方法對應的實現,會進入方法決議。

方法決議

方法的決議分為例項方法和類方法,因為兩者的過程都相似,所以這裡只講例項方法。

方法決議時會觸發resolveInstanceMethod:方法的呼叫,如果當前類實現該方法,並在該方法中使用 class_addMethod()動態的為引數sel關聯實現(imp),那麼在返回YES後會呼叫新關聯的imp,並快取。

如果在方法決議時,沒有動態的關聯實現,便會觸發訊息轉發。

訊息轉發

訊息的轉發有兩種形式forwardingTargetForSelectorforwardInvocation功能有所差別。

forwardingTargetForSelector:需要返回一個能夠響應sel訊息的物件。如果該物件無法響應傳入的選擇子會呼叫forwardInvocation:

forwardInvocation:呼叫前需要通過methodSignatureForSelector:方法提供方法簽名。

forwardInvocation:接受一個NSInvocation引數,該物件包含當前選擇子和物件。然而我們完全可以忽視這個引數做任何事情,因為只要這個方法實現,當前物件就不會再拋無法響應訊息的異常了。

所以需要謹慎的過載這個方法,否者如果某個未知方法沒有實現,卻不會丟擲異常,就無法察覺了。

最後如果沒有實現訊息轉發,會在根類NSObject中呼叫doesNotRecognizedSelector拋異常。

總結

從響應訊息的流程上來看,存在一些值得思考的地方。

應該儘量減少 Category 的數量,因為 Category 會作為元素新增到二維指標陣列,增加陣列的長度,也就增加方法查詢的時間消耗。

Category 的方法會先於主類被查詢到,如果 Category 使用了主類的同名方法,主類的實現會被覆蓋。

在方法決議時,會使用class_addMethod動態新增方法的實現,該方法會將新加的方法作為一個單一的 list 元素,新增到二維指標陣列,同樣會增加陣列的長度。

同時,該函式在新增新方法後會沖刷(flush)方法快取。其目的是為了防止先前存在同名的方法被呼叫過,被快取,再次呼叫會命中快取,而不會執行新新增的方法實現。

訊息轉發其實是變相的實現了多型,將當前類的訊息交給其他類處理,甚至忽略轉發的訊息而去做其他事情。

不過,到達訊息轉發需要經歷前面的四步,也是一筆不小的開銷,過多的依賴轉發來響應訊息會影響效能。

參考:

從原始碼看 ObjC 中訊息的傳送

objc 原始碼

相關文章