若想令類能理解某條消息,我們必須以程序碼實現出對應的方法才行。但是,編譯器在編譯時還無法知道類中有沒有對某個方法的實現。當對象接收到無法解讀的消息后,就會啟動“消息轉發”(message forwarding)機制,我們可以經由此過程告訴對象應該如何處理未知消息。
如果控制臺中看到下面的這種提示,那就說明你曾向某個對象發送過一條其無法解讀的消息,從而啟動了消息轉發機制,并將此消息轉發給了NSObject的默認實現。
上面這段異常信息是由NSObject的 “doesNotRecognizeSelector:”方法所拋出的,此異常表明,消息接收者的類型是 ViewController,而該接收者無法理解名為 doSomething 的選擇子。
在本例中,消息轉發過程以程序崩潰而告終,不過,開發者在編寫自己的類時,可于轉發過程中設置掛鉤,用以執行預定的邏輯,而不使應用程序崩潰。
消息轉發機制分為兩大階段。第一階段先征詢接收者所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子”(unknow selector),這叫做“動態方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉發機制”(full forwarding mechanism).
一、動態方法解析
如果該方法是實例方法,對象在接收到無法解讀的消息后,首先將調用其所屬類的 resolveInstanceMethod: 類方法。sel就是未知的選擇子,該方法返回一個boolean類型,表示這個類是否能新增一個實例方法處理此選擇子。
如果該方法是類方法,那么運行期系統就會調用resolveClassMethod:類方法。
使用上面方法的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類里面就可以了。
下面還是以上面的button為例,為其實現動態方法解析。
+ (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(@"動態添加了方法\"%@\" ,防止程序crash", NSStringFromSelector(_cmd));
}
控制臺打印如下:程序不會crash。
備援接收者
當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉發給其他接收者來處理。與該步驟對應的處理方法如下:
方法參數代表未知選擇子,若當前接收者能找到備援對象,則將其返回,若找不到,就返回nil。通過此方案,我們可以用“組合(composition)”來模擬“多重繼承”的某些特性。在一個對象內部,可能還有一系列其他對象,該對象可經由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些消息。
請注意,我們無法操作經由這一步所轉發的消息。若是想在發送給備援接收者之前先修改消息內容,那就得通過完整的消息轉發機制來做。
下面還是以上面的button為例,為其實現備援接收者。
聲明一個備援接收者的類
#import <Foundation/Foundation.h>
@interface MyForwardingTargetClass : NSObject
@end
#import "MyForwardingTargetClass.h"
@implementation MyForwardingTargetClass
// 不需要在.h中聲明,運行時會動態查找類中是否實現該方法
- (void)doSomething {
NSLog(@"備援接受者的方法調用了,程序沒有crash!!!");
}
@end
在VC中實現備援接收者的處理方法:
// 消息轉發機制 第一階段:備援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 備援接收者 只需要在.m中實現doSomething就可以防止crash
if ([NSStringFromSelector(aSelector) isEqualToString:@"doSomething"]) {
return [MyForwardingTargetClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
控制臺打印如下,程序沒有crash.
二、完整的消息轉發機制
如果轉發算法已經到了這一步,那只能啟動完整的消息轉發機制了。首先創建NSInvocation 對象,把與尚未處理的那條消息有關的全部細節都封于其中。此對象包含選擇子、(目標 target)、參數。在觸發NSInvocation對象時,“消息派發系統”將親自出馬,把消息指派給目標對象。
此步驟會調用下列方法來轉發消息:
這個方法可以實現的很簡單:只需要改變調用目標,使消息在新的目標上得以調用即可。然而這樣實現出來的方法與“備援接收者”方案所實現的等效,一般很少采用這么簡單的實現方式。
比較有用的實現方式為:在觸發消息前,先以某種方式改變消息內容,比如追加另一個參數,或者改變選擇子等等。
實現此方法時,若發現某調用操作不應由本類處理,則需調用超類的同名方法。這樣的話,繼承體系中的每個類都有機會處理此調用請求,直至NSObject。如果最后調用了NSObject類的方法,那么該方法還會繼而調用“doesNotRecognizeSelector:”以拋出異常,此異常表明選擇子最終未能得到處理。
下面還是以上面的button為例,為其實現完整的消息轉發機制。此處先簡單的實現下(和備援接收者實現方案等效):
創建一個類,處理vc不能處理的方法
#import <Foundation/Foundation.h>
@interface MethodCrashClass : NSObject
- (void)methodCrash:(NSInvocation *)invocation;
@end
#import "MethodCrashClass.h"
@implementation MethodCrashClass
- (void)methodCrash:(NSInvocation *)invocation {
NSLog(@"在類:%@中 未實現該方法:%@",NSStringFromClass([invocation.target class]),NSStringFromSelector(invocation.selector));
}
@end
控制臺打印如下,程序沒有crash。
那么問題來了!這些方法是在VC中實現的,如果我們想要給每個類都添加一個防止crash的方法呢?顯然這樣添加不是一個很好的選擇。
解決方案:
//創建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(@"在類:%@中 未實現該方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}
@end
控制臺打印如下,程序沒有crash。
因為在category中復寫了父類的方法,會出現下面的警告,
解決辦法就是在Xcode的Build Phases中的資源文件里,在對應的文件后面 -w ,忽略所有警告。
此處還有一點需要解釋的,就是在方法簽名中的Types:"v@:@",這些符號是什么意思呢?
其實這些符號就是返回值和方法參數對應的類型。可在Xcode中的開發者文檔中搜索Type Encodings就可看到符號對應的含義,此處不再列舉了。
消息轉發全流程
接收者在每一步中均有機會處理消息。步驟越往后,處理消息的代價越大。最好能在第一步處理完,這樣運行期系統可以把此方法緩存起來。如果這個類的實例稍后還會收到同名選擇子,則無須啟動消息轉發流程。
若想在第三步里把消息轉發給備援接收者,還不如把轉發操作提前到第二步。因為第三步只是修改了調用目標,這項改動放在第二步執行會更簡單,不然的話,還要創建并處理完整的NSIncocation。
demo放在GitHub上了,有需要的可以download下來查看.
可以利用消息轉發機制的三個步驟,選擇哪一步去改造比較合適呢?
這里我們選擇了第二步forwardingTargetForSelector
。引用 《大白健康系統—iOS APP運行時Crash自動修復系統》 的分析:
-
resolveInstanceMethod
需要在類的本身上動態添加它本身不存在的方法,這些方法對于該類本身來說冗余的。 -
forwardInvocation
可以通過 NSInvocation 的形式將消息轉發給多個對象,但是其開銷較大,需要創建新的 NSInvocation 對象,并且forwardInvocation
的函數經常被使用者調用,來做多層消息轉發選擇機制,不適合多次重寫。 -
forwardingTargetForSelector
可以將消息轉發給一個對象,開銷較小,并且被重寫的概率較低,適合重寫。
千里之行,始于足下。