前言:
unrecognized selector類型的crash是因為一個對象調用了一個不屬於它的方法導致的。要解決這種類型的crash,我們先要了解清楚它產生的具體原因和流程。本文先講了消息傳遞機制和消息轉發機制的流程,然后對消息轉發流程的一些函數的使用進行舉例,最后指出了對“unrecognized selector類型的crash”的防護措施。
一、消息傳遞機制和消息轉發機制
1. 消息傳遞機制(動態消息派發系統的工作過程)
當編譯器收到[someObject messageName:parameter]消息后,編譯器會將此消息轉換為調用標准的C語言函數objc_msgSend,如下所示:
objc_msgSend(someObject,@selector(messageName:),parameter)
該方法會去someObject所屬的類中搜尋其“方法列表”,如果能找到與messageName:相符的方法,就跳轉到實現代碼;找不到就沿着繼承體系繼續向上找;如果最終還是找不到,就執行“消息轉發”操作。
2. 消息轉發機制
消息轉發分兩大階段:
(1)動態方法解析:即征詢selector所屬的類的下列方法,看其是否能動態添加這個未知的選擇子:
// 缺失的selector是實例方法調用 +(BOOL)resolveInstanceMethod:(SEL)selector // 缺失的selector是類方法調用 +(BOOL)resolveClassMethod:(SEL)selector
該方法的參數就是那個未知的選擇子,其返回值Boolean類型,表示這個類是否能新增一個實例方法用以處理此選擇子。(@dynamic屬性沒有實現setter方法和getter方法,可以在“消息轉發”過程對其實現)
(2)消息轉發
(2.1)“備援接收者”方案----當前接收者第二次處理未知選擇子的機會:運行期系統通過下列方法問當前接收者,能不能把這條消息轉發給其它接收者來處理:
-(id)forwardingTargetForSelector:(SEL)selector
該方法的參數就是那個未知的選擇子,其返回值id類型,表示找到的備援對象,找不到就返回nil。(缺點:我們無法操作經由這一步所轉發的消息。)
(2.2) 完整的消息轉發
調用下列方法轉發消息:
-(void)forwardInvocation:(NSInvocation*)invocation
NSInvocation把尚未處理的那條消息有關的全部細節都封於其中,包括:選擇子、目標及參數。
(a)上面這個方法可以實現的很簡單:只需改變調用目標,使消息在新目標上得以調用即可(與“備援接收者”方案所實現的方法等效,很少有人采用)。
(b)比較有用的實現方式為:在觸發消息前,先以某種方式改變消息內容,比如追加另外一個參數,或是改換選擇子等等。
上面的步驟都不能解決問題的話,就會調用NSObject的doesNotRecognizeSelector拋出異常。
總結:
消息轉發的全流程,如下圖所示:
“消息轉發”全流程圖
二、舉例
1. 動態方法解析,即resolveInstanceMethod的使用:
(以動態方法解析來實現@dynamic屬性)
//EOCAutoDictionary.h @interface EOCAutoDictionary : NSObject @property(nonatomic, strong) NSDate *date; @end //EOCAutoDictionary.m #import "EOCAutoDictionary.h" #import <objc/runtime.h> @interface EOCAutoDictionary() @property(nonatomic, strong) NSMutableDictionary *backingStore; @end @implementation EOCAutoDictionary @dynamic date; - (id)init { if(self = [super init]) { _backingStore = [NSMutableDictionary new]; } return self; } + (BOOL) resolveInstanceMethod:(SEL)selector { //selector = "setDate:" 或 "date",_cmd = (SEL)"resolveInstanceMethod:" NSString *selectorString = NSStringFromSelector(selector); if([selectorString hasPrefix:@"set"]) { // 向類中動態的添加方法,第三個參數為函數指針,指向待添加的方法。最后一個參數表示待添加方法的“類型編碼” class_addMethod(self, selector,(IMP)autoDictionarySetter,"v@:@"); } else { class_addMethod(self, selector,(IMP)autoDictionaryGetter,"v@:@"); } return YES; } id autoDictionaryGetter(id self, SEL _cmd) { // 此時_cmd = (SEL)"date" // Get the backing store from the object EOCAutoDictionary *typeSelf = (EOCAutoDictionary *) self; NSMutableDictionary *backingStore = typeSelf.backingStore; //the key is simply the selector name NSString *key = NSStringFromSelector(_cmd); //Return the value return [backingStore objectForKey:key]; } void autoDictionarySetter(id self, SEL _cmd, id value) { // 此時_cmd = (SEL)"setDate:" // Get the backing store from the object EOCAutoDictionary *typeSelf = (EOCAutoDictionary *) self; NSMutableDictionary *backingStore = typeSelf.backingStore; /** The selector will be for example, "setDate:". * We need to remove the "set",":" and lowercase the first letter of the remainder. */ NSString *selectorString = NSStringFromSelector(_cmd); NSMutableString *key = [selectorString mutableCopy]; // Remove the ':' at the end [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)]; // Remove the 'set' prefix [key deleteCharactersInRange:NSMakeRange(0, 3)]; // Lowercase the first character NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar]; if(value) { [backingStore setObject:value forKey:key]; } else { [backingStore removeObjectForKey:key]; } } @end
使用date屬性的setter和getter代碼如下:
EOCAutoDictionary *dict = [EOCAutoDictionary new]; dict.date = [NSDate dateWithTimeIntervalSince1970:475372800]; NSLog(@"dict.date = %@", dict.date);
2. forwardingTargetForSelector的使用
注意:上面的resolveInstanceMethod返回YES的話,就無法調用forwardingTargetForSelector了。
下面的方法,對SLVForwardTarget的對象調用uppercaseString方法時,轉發給另一個對象"hello WorLD!"來執行uppercaseString方法。
@implementation SLVForwardTarget #pragma mark forwardingTargetForSelector -(id) forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(uppercaseString)){ return @"hello WorLD!"; } return nil; } @end
測試代碼:
SLVForwardTarget *ft = [SLVForwardTarget new]; NSString * s = [ft performSelector:@selector(uppercaseString)]; NSLog(@"%@",s); //輸出結果為:“HELLO WORLD!”
3. forwardInvocation的使用
改變調用目標,使消息在新目標上得以調用的例子:
// SLVForwardInvocation.h @interface SLVForwardInvocation : NSObject - (id)initWithTarget1:(id)t1 target2:(id)t2; @end // SLVForwardInvocation.m @interface SLVForwardInvocation() @property(nonatomic, strong)id realObject1; @property(nonatomic, strong)id realObject2; @end @implementation SLVForwardInvocation - (id)initWithTarget1:(id)t1 target2:(id)t2 { _realObject1 = t1; _realObject2 = t2; return self; } //系統check實例是否能response消息呢?如果實例本身就有相應的response,那么就會響應之,如果沒有系統就會發出methodSignatureForSelector消息,尋問它這個消息是否有效?有效就返回對應的方法簽名,無效則返回nil。消息轉發機制使用從這個方法中獲取的信息來創建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。
// Here, we ask the two real objects, realObject1 first, for their metho
// signatures, since we'll be forwarding the message to one or the other
// of them in -forwardInvocation:. If realObject1 returns a non-nil
// method signature, we use that, so in effect it has priority.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *sig; sig = [self.realObject1 methodSignatureForSelector:aSelector]; if (sig){ return sig; } sig = [self.realObject2 methodSignatureForSelector:aSelector]; if (sig){ return sig; } return nil; } // Invoke the invocation on whichever real object had a signature for it. - (void)forwardInvocation:(NSInvocation *)invocation { id target = [self.realObject1 methodSignatureForSelector:[invocation selector]] ? self.realObject1 : self.realObject2; [invocation invokeWithTarget:target]; //或者用下列方法 /* id target; if([self.realObject1 respondsToSelector:[invocation selector]]) { target = self.realObject1; } else if([self.realObject2 respondsToSelector:[invocation selector]]) { target = self.realObject2; } [invocation invokeWithTarget:target]; */ }
測試代碼:
NSMutableString *string = [NSMutableString new]; NSMutableArray *array = [NSMutableArray new]; id proxy = [[SLVForwardInvocation alloc] initWithTarget1:string target2:array]; // Note that we can't use appendFormat:, because vararg methods // cannot be forwarded! [proxy appendString:@"This "]; [proxy appendString:@"is "]; [proxy addObject:string]; [proxy appendString:@"a "]; [proxy appendString:@"test!"]; if ([[proxy objectAtIndex:0] isEqualToString:@"This is a test!"]) { NSLog(@"Appending successful."); } else { NSLog(@"Appending failed, got: '%@'", proxy); }
此處選擇子"appendString:"改變目標為mutableString類型,"addObject:"和"objectAtIndex:"改變目標為mutableArray類型。
三、unrecognized selector crash防護方案
根據上面的講解和舉例,我們知道,當一個函數找不到時,runtime提供了三種方式去補救:
(1)調用resolveInstanceMethod給個機會讓類添加實現這個函數;
(2)調用forwardingTargetForSelector讓別的對象去執行這個函數;
(3)調用forwardInvocation(函數執行器)靈活的將目標函數以其它形式執行。
第一種方案:
對於“unrecognized selector crash”,我們就可以利用消息轉發機制來進行補救。對於使用上面三步中的哪一步來改造比較合適,我們選擇第二步forwardingTargetForSelector。初步分析原因如下:上面的三步接收者均有機會處理消息。步驟越往后,處理消息的代價就越大。forwardInvocation要通過NSInvocation來執行函數,得創建和處理完整的NSInvocation,開銷比較大。但resolveInstanceMethod給類添加不存在的方法,有可能這個方法並不需要,比較多余。用forwardingTargetForSelector將消息轉發給一個對象,開銷較小。
防護方案如下:
NSObject的類別NSObject+Forwarding來重寫forwardingTargetForSelector方法,讓執行的目標轉移到SLVUnrecognizedSelectorSolveObject里,然后SLVUnrecognizedSelectorSolveObject添加新的方法對未知選擇子進行處理。在處理的這一塊兒,可以加上日志.
缺點:
(1)類里的forwardingTargetForSelector如果提前返回nil了,就沒辦法執行SLVStubProxy里的autoAddMethod方法。另外,未知選擇子對應的類里面如果有forwardInvocation方法的話,會優先執行SLVStubProxy里的autoAddMethod方法,而不會執行選擇子對應的類里面的forwardInvocation方法。 整個處理流程,完全是按照以上三種方式的前后順序執行,一旦一個方式解決了這個函數調用的問題,其它方法就不會執行。這里得注意工程代碼里,可能就是需要自己的類里處理未知選擇子的情況。
(2)還有一些selector如:"getServerAnswerForQuestion:reply:"、
"startArbitrationWithExpectedState:hostingPIDs:withSuppression:onConnected:"、
"_setTextColor:"、"setPresentationContextPrefersCancelActionShown:" 也會攔截到。本來這些selector系統會自己處理的,相當於這塊兒的攔截超前了,照這個比較大的缺陷來說,我們還是在第三步forwardInvocation來處理未知選擇子比較好,所以有了下面這個方案。
第二種方案:
消息轉發機制里的三個步驟處理未知選擇子,步驟越往后,處理消息的代價就越大。但是步驟越往前,我們越有可能攔截到系統的本來能處理的方法,這種方案是以犧牲效率來改善攔截的准確性的。
防護方案如下:
NSObject的類別NSObject+Forwarding來重寫forwardInvocation方法,考慮到諸如"_navigationControllerContentInsetAdjustment"的選擇子有可能系統會在自己的forwardInvocation方法里進行處理,所以此處先判斷系統的方法能否處理,系統的方法不能處理未知選擇子,再讓執行的目標轉移到未知選擇子處理對象SLVUnrecognizedSelectorSolveObject 里。然后SLVUnrecognizedSelectorSolveObject添加新的方法對未知選擇子進行處理。在處理的這一塊兒,可以加上日志信息。
以上兩種方案的代碼如下,其中用枚舉SLVUnrecognizedSelectorSolveScheme分別表示上面的兩種方案,可自行修改,這里推薦第二種方案:
// NSObject+Forwarding.m #import "NSObject+Forwarding.h" #import "SLVUnrecognizedSelectorSolveObject.h" #import <objc/runtime.h> typedef NS_ENUM(NSInteger, SLVUnrecognizedSelectorSolveScheme) { SLVUnrecognizedSelectorSolveScheme1, //第一種方案 SLVUnrecognizedSelectorSolveScheme2 //第二種方案 }; @implementation NSObject (Forwarding) + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SLVUnrecognizedSelectorSolveScheme scheme = SLVUnrecognizedSelectorSolveScheme2; if(scheme == SLVUnrecognizedSelectorSolveScheme1){ [[self class] swizzedMethod:@selector(forwardingTargetForSelector:) withMethod:@selector(newForwardingTargetForSelector:)]; }else if(scheme == SLVUnrecognizedSelectorSolveScheme2){ [[self class] swizzedMethod:@selector(methodSignatureForSelector:) withMethod:@selector(newMethodSignatureForSelector:)]; [[self class] swizzedMethod:@selector(forwardInvocation:) withMethod:@selector(newForwardInvocation:)]; } }); } +(void)swizzedMethod:(SEL)originalSelector withMethod:(SEL )swizzledSelector { Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); }else{ method_exchangeImplementations(originalMethod, swizzledMethod); } } #pragma mark forwardTarget -(id) newForwardingTargetForSelector:(SEL)aSelector { SLVUnrecognizedSelectorSolveObject *obj = [SLVUnrecognizedSelectorSolveObject sharedInstance]; return obj; } - (NSMethodSignature *)newMethodSignatureForSelector:(SEL)sel{ SLVUnrecognizedSelectorSolveObject *unrecognizedSelectorSolveObject = [SLVUnrecognizedSelectorSolveObject sharedInstance]; return [self newMethodSignatureForSelector:sel]?:[unrecognizedSelectorSolveObject newMethodSignatureForSelector:sel];
}
- (void)newForwardInvocation:(NSInvocation *)anInvocation{
if([self newMethodSignatureForSelector:anInvocation.selector]){
[self newForwardInvocation:anInvocation];
return;
}
SLVUnrecognizedSelectorSolveObject *unrecognizedSelectorSolveObject = [SLVUnrecognizedSelectorSolveObject sharedInstance]; if([self methodSignatureForSelector:anInvocation.selector]){ [anInvocation invokeWithTarget:unrecognizedSelectorSolveObject]; } } // SLVUnrecognizedSelectorSolveObject.m #import "SLVUnrecognizedSelectorSolveObject.h" #import <objc/runtime.h> @implementation SLVUnrecognizedSelectorSolveObject + (instancetype) sharedInstance{ static SLVUnrecognizedSelectorSolveObject *unrecognizedSelectorSolveObject; static dispatch_once_t once_token; dispatch_once(&once_token, ^{ unrecognizedSelectorSolveObject = [[SLVUnrecognizedSelectorSolveObject alloc] init]; }); return unrecognizedSelectorSolveObject; } + (BOOL) resolveInstanceMethod:(SEL)selector { // 向類中動態的添加方法,第三個參數為函數指針,指向待添加的方法。最后一個參數表示待添加方法的“類型編碼” class_addMethod([self class], selector,(IMP)autoAddMethod,"v@:@"); return YES; } id autoAddMethod(id self, SEL _cmd) { //可以在此加入日志信息,棧信息的獲取等,方便后面分析和改進原來的代碼。
NSLog(@"unrecognized selector: %@",NSStringFromSelector(_cmd));
return 0;
}
PS:以上代碼自己寫的,有待進一步檢驗和改進。