若想令類能理解某條消息,我們必須以程序碼實(shí)現(xiàn)出對(duì)應(yīng)的方法才行。但是,編譯器在編譯時(shí)還無(wú)法知道類中有沒(méi)有對(duì)某個(gè)方法的實(shí)現(xiàn)。當(dāng)對(duì)象接收到無(wú)法解讀的消息后,就會(huì)啟動(dòng)“消息轉(zhuǎn)發(fā)”(message forwarding)機(jī)制,我們可以經(jīng)由此過(guò)程告訴對(duì)象應(yīng)該如何處理未知消息。
如果控制臺(tái)中看到下面的這種提示,那就說(shuō)明你曾向某個(gè)對(duì)象發(fā)送過(guò)一條其無(wú)法解讀的消息,從而啟動(dòng)了消息轉(zhuǎn)發(fā)機(jī)制,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認(rèn)實(shí)現(xiàn)。

上面這段異常信息是由NSObject的 “doesNotRecognizeSelector:”方法所拋出的,此異常表明,消息接收者的類型是 ViewController,而該接收者無(wú)法理解名為 doSomething 的選擇子。
在本例中,消息轉(zhuǎn)發(fā)過(guò)程以程序崩潰而告終,不過(guò),開(kāi)發(fā)者在編寫(xiě)自己的類時(shí),可于轉(zhuǎn)發(fā)過(guò)程中設(shè)置掛鉤,用以執(zhí)行預(yù)定的邏輯,而不使應(yīng)用程序崩潰。
消息轉(zhuǎn)發(fā)機(jī)制分為兩大階段。第一階段先征詢接收者所屬的類,看其是否能動(dòng)態(tài)添加方法,以處理當(dāng)前這個(gè)“未知的選擇子”(unknow selector),這叫做“動(dòng)態(tài)方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”(full forwarding mechanism).
一、動(dòng)態(tài)方法解析
如果該方法是實(shí)例方法,對(duì)象在接收到無(wú)法解讀的消息后,首先將調(diào)用其所屬類的 resolveInstanceMethod: 類方法。sel就是未知的選擇子,該方法返回一個(gè)boolean類型,表示這個(gè)類是否能新增一個(gè)實(shí)例方法處理此選擇子。
如果該方法是類方法,那么運(yùn)行期系統(tǒng)就會(huì)調(diào)用resolveClassMethod:類方法。
使用上面方法的前提是:相關(guān)方法的實(shí)現(xiàn)代碼已經(jīng)寫(xiě)好,只等著運(yùn)行的時(shí)候動(dòng)態(tài)插在類里面就可以了。
下面還是以上面的button為例,為其實(shí)現(xiàn)動(dòng)態(tài)方法解析。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"doSomething"]) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"動(dòng)態(tài)添加了方法\"%@\" ,防止程序crash", NSStringFromSelector(_cmd));
}
控制臺(tái)打印如下:程序不會(huì)crash。

備援接收者
當(dāng)前接收者還有第二次機(jī)會(huì)能處理未知的選擇子,在這一步中,運(yùn)行期系統(tǒng)會(huì)問(wèn)它:能不能把這條消息轉(zhuǎn)發(fā)給其他接收者來(lái)處理。與該步驟對(duì)應(yīng)的處理方法如下:
方法參數(shù)代表未知選擇子,若當(dāng)前接收者能找到備援對(duì)象,則將其返回,若找不到,就返回nil。通過(guò)此方案,我們可以用“組合(composition)”來(lái)模擬“多重繼承”的某些特性。在一個(gè)對(duì)象內(nèi)部,可能還有一系列其他對(duì)象,該對(duì)象可經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對(duì)象返回,這樣的話,在外界看來(lái),好像是該對(duì)象親自處理了這些消息。
請(qǐng)注意,我們無(wú)法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息。若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容,那就得通過(guò)完整的消息轉(zhuǎn)發(fā)機(jī)制來(lái)做。
下面還是以上面的button為例,為其實(shí)現(xiàn)備援接收者。
聲明一個(gè)備援接收者的類
#import <Foundation/Foundation.h>
@interface MyForwardingTargetClass : NSObject
@end
#import "MyForwardingTargetClass.h"
@implementation MyForwardingTargetClass
// 不需要在.h中聲明,運(yùn)行時(shí)會(huì)動(dòng)態(tài)查找類中是否實(shí)現(xiàn)該方法
- (void)doSomething {
NSLog(@"備援接受者的方法調(diào)用了,程序沒(méi)有crash!!!");
}
@end
在VC中實(shí)現(xiàn)備援接收者的處理方法:
// 消息轉(zhuǎn)發(fā)機(jī)制 第一階段:備援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 備援接收者 只需要在.m中實(shí)現(xiàn)doSomething就可以防止crash
if ([NSStringFromSelector(aSelector) isEqualToString:@"doSomething"]) {
return [MyForwardingTargetClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
控制臺(tái)打印如下,程序沒(méi)有crash.

二、完整的消息轉(zhuǎn)發(fā)機(jī)制
如果轉(zhuǎn)發(fā)算法已經(jīng)到了這一步,那只能啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制了。首先創(chuàng)建NSInvocation 對(duì)象,把與尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中。此對(duì)象包含選擇子、(目標(biāo) target)、參數(shù)。在觸發(fā)NSInvocation對(duì)象時(shí),“消息派發(fā)系統(tǒng)”將親自出馬,把消息指派給目標(biāo)對(duì)象。
此步驟會(huì)調(diào)用下列方法來(lái)轉(zhuǎn)發(fā)消息:
這個(gè)方法可以實(shí)現(xiàn)的很簡(jiǎn)單:只需要改變調(diào)用目標(biāo),使消息在新的目標(biāo)上得以調(diào)用即可。然而這樣實(shí)現(xiàn)出來(lái)的方法與“備援接收者”方案所實(shí)現(xiàn)的等效,一般很少采用這么簡(jiǎn)單的實(shí)現(xiàn)方式。
比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另一個(gè)參數(shù),或者改變選擇子等等。
實(shí)現(xiàn)此方法時(shí),若發(fā)現(xiàn)某調(diào)用操作不應(yīng)由本類處理,則需調(diào)用超類的同名方法。這樣的話,繼承體系中的每個(gè)類都有機(jī)會(huì)處理此調(diào)用請(qǐng)求,直至NSObject。如果最后調(diào)用了NSObject類的方法,那么該方法還會(huì)繼而調(diào)用“doesNotRecognizeSelector:”以拋出異常,此異常表明選擇子最終未能得到處理。
下面還是以上面的button為例,為其實(shí)現(xiàn)完整的消息轉(zhuǎn)發(fā)機(jī)制。此處先簡(jiǎn)單的實(shí)現(xiàn)下(和備援接收者實(shí)現(xiàn)方案等效):
創(chuàng)建一個(gè)類,處理vc不能處理的方法
#import <Foundation/Foundation.h>
@interface MethodCrashClass : NSObject
- (void)methodCrash:(NSInvocation *)invocation;
@end
#import "MethodCrashClass.h"
@implementation MethodCrashClass
- (void)methodCrash:(NSInvocation *)invocation {
NSLog(@"在類:%@中 未實(shí)現(xiàn)該方法:%@",NSStringFromClass([invocation.target class]),NSStringFromSelector(invocation.selector));
}
@end
控制臺(tái)打印如下,程序沒(méi)有crash。

那么問(wèn)題來(lái)了!這些方法是在VC中實(shí)現(xiàn)的,如果我們想要給每個(gè)類都添加一個(gè)防止crash的方法呢?顯然這樣添加不是一個(gè)很好的選擇。
解決方案:
//創(chuàng)建NSObject的分類
#import <Foundation/Foundation.h>
@interface NSObject (crashLog)
@end
#import "NSObject+crashLog.h"
@implementation NSObject (crashLog)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 方法簽名
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"在類:%@中 未實(shí)現(xiàn)該方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}
@end
控制臺(tái)打印如下,程序沒(méi)有crash。

因?yàn)樵赾ategory中復(fù)寫(xiě)了父類的方法,會(huì)出現(xiàn)下面的警告,
解決辦法就是在Xcode的Build Phases中的資源文件里,在對(duì)應(yīng)的文件后面 -w ,忽略所有警告。
此處還有一點(diǎn)需要解釋的,就是在方法簽名中的Types:"v@:@",這些符號(hào)是什么意思呢?
其實(shí)這些符號(hào)就是返回值和方法參數(shù)對(duì)應(yīng)的類型??稍赬code中的開(kāi)發(fā)者文檔中搜索Type Encodings就可看到符號(hào)對(duì)應(yīng)的含義,此處不再列舉了。
消息轉(zhuǎn)發(fā)全流程

接收者在每一步中均有機(jī)會(huì)處理消息。步驟越往后,處理消息的代價(jià)越大。最好能在第一步處理完,這樣運(yùn)行期系統(tǒng)可以把此方法緩存起來(lái)。如果這個(gè)類的實(shí)例稍后還會(huì)收到同名選擇子,則無(wú)須啟動(dòng)消息轉(zhuǎn)發(fā)流程。
若想在第三步里把消息轉(zhuǎn)發(fā)給備援接收者,還不如把轉(zhuǎn)發(fā)操作提前到第二步。因?yàn)榈谌街皇切薷牧苏{(diào)用目標(biāo),這項(xiàng)改動(dòng)放在第二步執(zhí)行會(huì)更簡(jiǎn)單,不然的話,還要?jiǎng)?chuàng)建并處理完整的NSIncocation。
demo放在GitHub上了,有需要的可以download下來(lái)查看.
可以利用消息轉(zhuǎn)發(fā)機(jī)制的三個(gè)步驟,選擇哪一步去改造比較合適呢?
這里我們選擇了第二步forwardingTargetForSelector。引用 《大白健康系統(tǒng)—iOS APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)》 的分析:
-
resolveInstanceMethod需要在類的本身上動(dòng)態(tài)添加它本身不存在的方法,這些方法對(duì)于該類本身來(lái)說(shuō)冗余的。 -
forwardInvocation可以通過(guò) NSInvocation 的形式將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象,但是其開(kāi)銷較大,需要?jiǎng)?chuàng)建新的 NSInvocation 對(duì)象,并且forwardInvocation的函數(shù)經(jīng)常被使用者調(diào)用,來(lái)做多層消息轉(zhuǎn)發(fā)選擇機(jī)制,不適合多次重寫(xiě)。 -
forwardingTargetForSelector可以將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象,開(kāi)銷較小,并且被重寫(xiě)的概率較低,適合重寫(xiě)。
千里之行,始于足下。





