消息機制是 Objective-C 語言的基礎,也是它動態化的核心所在。筆者在閱讀 objc 源碼以後,對該語言的使用有了一些新的思考。ios
對象或者類在響應消息時,最多會經歷 5 個過程:git
消息的響應過程實際上是根據選擇子(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
,並緩存。
若是在方法決議時,沒有動態的關聯實現,便會觸發消息轉發。
消息的轉發有兩種形式forwardingTargetForSelector
和forwardInvocation
功能有所差異。
forwardingTargetForSelector:
須要返回一個可以響應sel
消息的對象。若是該對象沒法響應傳入的選擇子會調用forwardInvocation:
。
在forwardInvocation:
調用前須要經過methodSignatureForSelector:
方法提供方法簽名。
forwardInvocation:
接受一個NSInvocation
參數,該對象包含當前選擇子和對象。然而咱們徹底能夠忽視這個參數作任何事情,由於只要這個方法實現,當前對象就不會再拋沒法響應消息的異常了。
因此須要謹慎的重載這個方法,否者若是某個未知方法沒有實現,卻不會拋出異常,就沒法察覺了。
最後若是沒有實現消息轉發,會在根類NSObject
中調用doesNotRecognizedSelector
拋異常。
從響應消息的流程上來看,存在一些值得思考的地方。
應該儘可能減小 Category 的數量,由於 Category 會做爲元素添加到二維指針數組,增長數組的長度,也就增長方法查找的時間消耗。
Category 的方法會先於主類被查找到,若是 Category 使用了主類的同名方法,主類的實現會被覆蓋。
在方法決議時,會使用class_addMethod
動態添加方法的實現,該方法會將新加的方法做爲一個單一的 list 元素,添加到二維指針數組,一樣會增長數組的長度。
同時,該函數在添加新方法後會沖刷(flush)方法緩存。其目的是爲了防止先前存在同名的方法被調用過,被緩存,再次調用會命中緩存,而不會執行新添加的方法實現。
消息轉發實際上是變相的實現了多態,將當前類的消息交給其餘類處理,甚至忽略轉發的消息而去作其餘事情。
不過,到達消息轉發須要經歷前面的四步,也是一筆不小的開銷,過多的依賴轉發來響應消息會影響性能。
參考: