消息轉發機制的回顧以及涉及的幾個方法的備忘
一、OC消息發送原理 + 消息轉發機制
1、由於OC的動態特性,只有當程序運行起來之后,才知道要真正執行哪個函數(動態綁定)。在編譯過程向類發送了其無法理解的消息並不會報錯,因為在運行時,我們可以改變對象調用的方法、向類中添加方法。
2、OC消息發送原理、方法查找過程
(1)調用一個方法(包括respondsToSelector),編譯器將OC代碼,轉換成C函數,給對象發送消息 : void objc_msgSend(id self, SEL cmd,...) ,第一個參數是接收者,第二個參數是方法(名),后面是消息的參數。
(2)objc_msgSend查找方法的過程:
- 實例對象根據其isa指針,找到其所屬的class,然后遍歷其methodLists,如果找到則根據IMP函數指針去調用,並且緩存(objc_cache);如果沒有找到,那么根據這個類的super_class找到其父類,再看其父類是否能相應這個方法就可以了,直到super_class為nil時,就無法響應這個方法了,此時就觸發消息轉發機制。
- 當使用類名調用類方法(+方法)時,只需要根據class的isa指針,找到其meta-class,然后通過meta-class的methodLists找到相應的方法既可(“類”是“元類”的對象)。
3、如果對象接收到無法解讀的消息后(未查詢到該方法),就會啟動“消息轉發”機制,我們可在此過程告訴對象應該如何處理未知消息。如果我們不做任何處理,或處理無效,則會調用doesNotRecognizeSelector:,造成異常崩潰:unrecognized selector sent to instance 0xxx
二、消息轉發機制的處理過程
消息轉發機制依次的三個過程:
1、動態方法解析
第一階段,先征詢接收者所屬的類,是否需要動態的添加方法,用來處理當前未找到的方法。對象在無法解讀消息時會首先調用所屬類的下列類方法,來判斷是否能接收消息:
- + (BOOL) resolveInstanceMethod:(SEL)selector,參數為那個未知的選擇子,返回值表示這個類能否新增一個實例方法處理此選擇子。
- 如果是類方法 ,則調用 + (BOOL) resolveClassMethod:(SEL)selector,有一點要注意,類方法的添加需要在其“元類”里面。
舉例:
//消息轉發機制的第一步 :動態方法解析 + (BOOL)resolveInstanceMethod:(SEL)sel{ NSString *selName = NSStringFromSelector(sel); if ([selName hasPrefix:@"doSomeThing"]) {//判斷特定無法響應的方法 class_addMethod(self, sel, (IMP)otherOneDoSomeThing, "v@:");//動態添加響應方法 return YES; } return [super resolveInstanceMethod:sel]; } //動態將實現轉到這個函數(或者就是單純的添加doSomeThing方法) void otherOneDoSomeThing(id self ,SEL _cmd){ NSLog(@"class:%@, sel:%s",self,sel_getName(_cmd)); NSLog(@"原對象無法響應該消息,在動態方法解析時添加了一個方法來處理該消息"); }
2、備用的接收者
第二階段,如果動態方法解析沒有發現添加的方法,那么嘗試轉發給其他對象來處理這個方法。該步驟調用的方法是:
- - (id) forwardingTargetForSelector:(SEL)selector
舉例:
- (id)forwardingTargetForSelector:(SEL)aSelector{ NSString * selString = NSStringFromSelector(aSelector); if([@"doSomeThing" isEqualToString:selString]){ OtherObject *someone = [[OtherObject alloc] init];//備選對象 if ([someone respondsToSelector:aSelector]) { return someone;//如果可以響應該方法,則直接轉交新對象處理 } } return [super forwardingTargetForSelector:aSelector];//如果無合適的備選對象,則繼續轉發 }
3、完整的消息轉發機制
第三階段,如果沒有可用的備選者,那么系統就會把消息所有相關內容封裝成一個NSInvocation對象,再做最后的嘗試,啟動完整的消息轉發。先調用methodSignatureForSelector:獲取方法簽名,然后再調用forwardInvocation:進行處理,這一步的處理可以直接轉發給其它對象,即和第二步的效果等效,但是很少有人這么干,因為消息處理越靠后,就表示處理消息的成本越大,性能的開銷就越大。所以,在這種方式下,一般會改變消息內容,比如增加參數,改變選擇子等等,具體根據實際情況而定。
- - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- - (void)forwardInvocation:(NSInvocation *)anInvocation
舉例
//獲取方法簽名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSString *method = NSStringFromSelector(aSelector); if ([@"doSomeThing" isEqualToString:method]) { /* 手動創建簽名 寫法例子一 v@:@ 字符說明:(1)v:返回值類型void;(2)@:id類型,執行sel的對象;(3): SEL;(4)@:參數 寫法例子二 @@: 字符說明:(1)@:返回值類型id;(2)@:id類型,執行sel的對象;(3):SEL */ NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"]; return signature; } return nil; } - (void)forwardInvocation:(NSInvocation *)anInvocation { //*----------- 處理方式一:不改變sel -------------*/ // 拿到這個消息 SEL selector = [anInvocation selector]; // 轉發消息 AnotherObject *otherObject = [[AnotherObject alloc] init]; if ([otherObject respondsToSelector:selector]) { // 調用這個對象,進行轉發 [anInvocation invokeWithTarget:otherObject]; } else { [super forwardInvocation:anInvocation]; } //*---------------------------------------------*/ //*----------- 處理方式二:改變sel -------------*/ SEL selector = @selector(myAnotherMethod:); NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"]; anInvocation = [NSInvocation invocationWithMethodSignature:signature]; [anInvocation setTarget:self]; [anInvocation setSelector:@selector(myAnotherMethod:)]; NSString *param = @"參數"; // 消息的第一個參數是self,第二個參數是選擇子,所以"參數"是第三個參數 [anInvocation setArgument:¶m atIndex:2]; if ([self respondsToSelector:selector]) {//如果自己響應,就自己處理 [anInvocation invokeWithTarget:self]; return; } else { AnotherObject * otherObject = [[AnotherObject alloc] init]; if ([otherObject respondsToSelector:selector]) {//交給另外的對象來處理 [anInvocation invokeWithTarget:otherObject]; return; } } [super forwardInvocation:anInvocation]; //*---------------------------------------------*/ } //類中的另一個方法,來處理消息 - (void)myAnotherMethod:(NSString*)para { NSLog(@"交給我自己的另一個方法來處理:%@", para); }
三、應用場景
1、 為@dynamic實現方法
2、間接實現多繼承
- JSPatch,通過消息轉發機制來進行JS和OC的交互,從而實現iOS的熱更新。
- 雖然蘋果大力整改熱更新讓JSPatch的審核通過率在有一段時間里面無法過審,但是后面bang神對源碼進行代碼混淆之后,基本上是可以過審了。
- 下面截圖只摘出來用到消息轉發的部分:關鍵點就是在第三階段,通過invocation拿到方法參數,然后傳給JS,調用JS的實現函數。

四、總結
1、消息轉發機制的時機圖示
2、簡單理解
(1)首先,若對象無法響應某個方法調用,則進入消息轉發流程。
(2)開始第一步,通過運行時的動態方法解析,可以將需要的某個方法,加入到類中。
(3)上一步失敗,開始第二步,將消息轉發給其他對象處理。
(4)上述兩步失敗,啟動完整的消息轉發機制,通過封裝NSInvocation,明確指出方法的響應者(甚至改變SEL)。
(5)上述都失敗,拋出異常。