objc_object, objc_class 以及 Ojbc_method
在 Objective-C 中,每個類都有一個isa指針,在isa結構體中有一個objc_method_list(該類的方法列表),每個方法是objc_method。一個 objc_method 結構體中包含函數名,也就是SEL,有表示函數類型的字符串 (見 Type Encoding) 、方法參數類型以及函數的實現IMP。objc_method結構體如下:
struct objc_method {
? ? SEL _Nonnull method_name ? ? ? ? ?//方法名稱 ? ? ? ? ??
? ? char * _Nullable method_types ? ? ?//方法參數類型 ? ? ? ? ? ? ? ? ? ??
? ? IMP _Nonnull method_imp ? ? ? ? ? ? //方法實現地址
} ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
先講一下我理解的消息發送(調用方法),在OC中調用一個方法,本質上是給一個對象發送了一條消息(類方法也一樣,類是元類的對象)。比如[obj foo]調用obj的foo方法相當于objc_msgSend(obj, foo) ,即給obj發送foo消息。
方法調用的實現步驟如下(對象方法):
1.從isa結構體的objc_cache中查找是否緩存了該方法,如果緩存了則去方法實現地址實現該方法,結束。
2.若緩存中沒有,則從isa的objc_method_list中查找該方法,找到,則跳到方法的實現實現該方法,然后把該方法的method_name作為key,method_imp作為value存入objc_cache中,結束。
3.若在該isa中沒有找到方法,則通過isa的super_class找到父類,在他父類中重復第一步和第二步,若還沒有找到,則繼續往父類查找。
objc_cache存在的好處:
一個 class 往往只有 20% 的函數會被經常調用,可能占總調用次數的 80% 。每個消息都需要遍歷一次 objc_method_list 并不合理。如果把經常被調用的函數緩存下來,那可以大大提高函數查詢的效率。這就是objc_cache的作用。 - 再找到 foo 之后,把 foo 的 method_name 作為 key ,method_imp 作為 value 給存起來。當再次收到 foo 消息的時候,可以直接在 cache 里找到,避免去遍歷 objc_method_list。
動態方法解析和轉發
調用方法后如果在運行時經過了上述的步驟還是沒有找到該方法(調用了沒有實現的方法),一般情況程序會在運行時掛掉并拋出 unrecognized selector sent to … 的異常。但在異常拋出前,Objective-C 的運行時會給你三次拯救程序的機會:
1.Method Resolution(動態方法解析)
Objective-C 運行時會調用 +resolveInstanceMethod: 或者 +resolveClassMethod:,可以提供一個函數實現。如果你添加了函數并返回 YES(如果在返回NO前,給class添加了該方法的實現,則仍然會調用該方法), 那運行時系統就會重新啟動一次消息發送的過程。
如:創建一個類Test,聲明一個對象方法和一個類方法,并不實現他們
運行時,在遇到沒有實現的方法時,首先會調用所屬類(Test)的類方法+resolveInstanceMethod:(對象方法)或者+resolveClassMethod:(類方法)。在這個方法中,可以為該未知消息新增一個“處理方法”。
使用步驟:
1.聲明一個函數實現,即下面的void functionForMethod(id obj, SEL _cmd){}。
2.使用class_addMethod函數把該實現動態添加到類里面。此時就會調用聲明的函數,而不會奔潰。
如果 resolve 方法返回 NO,或者未實現resolve方法(默認是沒有其他函數可以接受該消息的),運行時就會移到下一步:
2.Message Forwarding(消息轉發)
動態方法解析無法處理消息的時候,會走消息轉發。如果目標對象(Test)實現了 -forwardingTargetForSelector: ,Runtime 這時就會調用這個方法,把這個消息轉發給其他對象去實現。(測試只能轉發對象方法)。
消息轉發的接收者不能是自己,需要新建一個類來接收轉發的消息。
新建RuntimeMethodHelper類,實現需要接收的轉發消息
在Test中實現-forwardingTargetForSelector:方法,此時會調用RuntimeMethodHelper中的hello方法
如果不實現該方法或者返回的是nil或者self,則會走創建一個 NSInvocation 對象的消息轉發。
3.完整消息轉發(創建NSInvocation)
這一步是 Runtime 最后一次挽救的機會。
首先它會發送 -methodSignatureForSelector: 消息獲得函數的參數和返回值類型。
如果 -methodSignatureForSelector: 返回 nil ,Runtime 則會發出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。
如果返回了一個函數簽名,Runtime 就會創建一個 NSInvocation 對象并發送 -forwardInvocation: 消息給當前對象。
NSInvocation 實際上就是對一個消息的描述,包括selector 以及參數等信息。所以你可以在 -forwardInvocation: 里修改傳進來的 NSInvocation 對象,然后發送 -invokeWithTarget: 消息,傳進去一個新的目標(響應該方法的對象):
Cocoa 里很多地方都利用到了消息傳遞機制來對語言進行擴展,如 Proxies、NSUndoManager 跟 Responder Chain。NSProxy 就是專門用來作為代理轉發消息的;NSUndoManager 截取一個消息之后再發送;而 Responder Chain 保證一個消息轉發給合適的響應者。
總結
Objective-C 中給一個對象發送消息會經過以下幾個步驟:
在對象類的 dispatch table 中嘗試找到該消息。如果找到了,跳到相應的函數IMP去執行實現代碼;
如果沒有找到,Runtime 會發送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve 這個消息;
如果 resolve 方法返回 NO,Runtime 就發送 -forwardingTargetForSelector: 允許你把這個消息轉發給另一個對象;
如果沒有新的目標對象返回, Runtime 就會發送 -methodSignatureForSelector:和 -forwardInvocation: 消息。你可以發送 -invokeWithTarget: 消息來手動轉發消息或者發送 -doesNotRecognizeSelector: 拋出異常。