訊息機制是 Objective-C 語言的基礎,也是它動態化的核心所在。筆者在閱讀 objc 原始碼之後,對該語言的使用有了一些新的思考。
物件或者類在響應訊息時,最多會經歷 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
,並快取。
如果在方法決議時,沒有動態的關聯實現,便會觸發訊息轉發。
訊息轉發
訊息的轉發有兩種形式forwardingTargetForSelector
和forwardInvocation
功能有所差別。
forwardingTargetForSelector:
需要返回一個能夠響應sel
訊息的物件。如果該物件無法響應傳入的選擇子會呼叫forwardInvocation:
。
在forwardInvocation:
呼叫前需要通過methodSignatureForSelector:
方法提供方法簽名。
forwardInvocation:
接受一個NSInvocation
引數,該物件包含當前選擇子和物件。然而我們完全可以忽視這個引數做任何事情,因為只要這個方法實現,當前物件就不會再拋無法響應訊息的異常了。
所以需要謹慎的過載這個方法,否者如果某個未知方法沒有實現,卻不會丟擲異常,就無法察覺了。
最後如果沒有實現訊息轉發,會在根類NSObject
中呼叫doesNotRecognizedSelector
拋異常。
總結
從響應訊息的流程上來看,存在一些值得思考的地方。
應該儘量減少 Category 的數量,因為 Category 會作為元素新增到二維指標陣列,增加陣列的長度,也就增加方法查詢的時間消耗。
Category 的方法會先於主類被查詢到,如果 Category 使用了主類的同名方法,主類的實現會被覆蓋。
在方法決議時,會使用class_addMethod
動態新增方法的實現,該方法會將新加的方法作為一個單一的 list 元素,新增到二維指標陣列,同樣會增加陣列的長度。
同時,該函式在新增新方法後會沖刷(flush)方法快取。其目的是為了防止先前存在同名的方法被呼叫過,被快取,再次呼叫會命中快取,而不會執行新新增的方法實現。
訊息轉發其實是變相的實現了多型,將當前類的訊息交給其他類處理,甚至忽略轉發的訊息而去做其他事情。
不過,到達訊息轉發需要經歷前面的四步,也是一筆不小的開銷,過多的依賴轉發來響應訊息會影響效能。
參考: