重拾 ObjC 消息機制

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

對象或者類在響應消息時,最多會經歷 5 個過程:git

  1. 查找當前類的緩存
  2. 在當前類的方法列表查找
  3. 在父類緩存及方法列表查找
  4. 消息決議
  5. 消息轉發

消息的響應過程實際上是根據選擇子(sel)查找對應的函數實現(imp)的過程。github

查找當前類的緩存

發送消息的objc_msgSend函數會經過cache_getImp函數查找緩存,由於全部方法的執行,都會調用這兩個函數,爲了提升性能,這兩個函數由彙編實現。數組

若是方法緩存沒有命中,會在當前類的methods中查找。緩存

查找當前類的方法列表

當前類的方法列表底層是一個二維method_list_t數組,存儲當前類以及全部 category 的方法。二維的方法列表是在 runtime 初始化的時候構建的,先添加主類的方法列表,而後依次添加 category 的,後添加的列表會放在數組的前面。函數

在查找時,會依次遍歷二維數組中的每一個元素列表,而後在列表中使用二分查找,直到找到選擇子(sel)對應的方法實現(imp)才終止。找到後使用log_and_fill_cache寫入當前類的緩存。性能

若是這個過程沒有找到,會進入父類查找。指針

在父類緩存及方法列表查找

在查找父類的方法列表前,會先查找父類的方法緩存,若是緩存沒有命中才會遍歷方法列表查找。查找過程和在當前類查找有點區別是,若是在緩存中找到消息轉發的 imp _objc_msgForward_impcache,會中止查找,直接進入消息轉發。code

若是在父類中找到對應的實現,會將該方法緩存到當前類中。對象

若是直到 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 源碼

相關文章
相關標籤/搜索