前言:
unrecognized selector類型的crash是因?yàn)橐粋€對象調(diào)用了一個不屬于它的方法導(dǎo)致的。要解決這種類型的crash,我們先要了解清楚它產(chǎn)生的具體原因和流程。本文先講了消息傳遞機(jī)制和消息轉(zhuǎn)發(fā)機(jī)制的流程,然后對消息轉(zhuǎn)發(fā)流程的一些函數(shù)的使用進(jìn)行舉例,最后指出了對“unrecognized selector類型的crash”的防護(hù)措施。
一、消息傳遞機(jī)制和消息轉(zhuǎn)發(fā)機(jī)制
1. ?消息傳遞機(jī)制(動態(tài)消息派發(fā)系統(tǒng)的工作過程)
當(dāng)編譯器收到[someObject messageName:parameter]消息后,編譯器會將此消息轉(zhuǎn)換為調(diào)用標(biāo)準(zhǔn)的C語言函數(shù)objc_msgSend,如下所示:
objc_msgSend(someObject,@selector(messageName:),parameter)
?該方法會去someObject所屬的類中搜尋其“方法列表”,如果能找到與messageName:相符的方法,就跳轉(zhuǎn)到實(shí)現(xiàn)代碼;找不到就沿著繼承體系繼續(xù)向上找;如果最終還是找不到,就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。
2. 消息轉(zhuǎn)發(fā)機(jī)制
消息轉(zhuǎn)發(fā)分兩大階段:
(1)動態(tài)方法解析:即征詢selector所屬的類的下列方法,看其是否能動態(tài)添加這個未知的選擇子:
//? 缺失的selector是實(shí)例方法調(diào)用+(BOOL)resolveInstanceMethod:(SEL)selector
//? 缺失的selector是類方法調(diào)用+(BOOL)resolveClassMethod:(SEL)selector
該方法的參數(shù)就是那個未知的選擇子,其返回值Boolean類型,表示這個類是否能新增一個實(shí)例方法用以處理此選擇子。(@dynamic屬性沒有實(shí)現(xiàn)setter方法和getter方法,可以在“消息轉(zhuǎn)發(fā)”過程對其實(shí)現(xiàn))
(2)消息轉(zhuǎn)發(fā)
(2.1)“備援接收者”方案----當(dāng)前接收者第二次處理未知選擇子的機(jī)會:運(yùn)行期系統(tǒng)通過下列方法問當(dāng)前接收者,能不能把這條消息轉(zhuǎn)發(fā)給其它接收者來處理:
-(id)forwardingTargetForSelector:(SEL)selector
該方法的參數(shù)就是那個未知的選擇子,其返回值id類型,表示找到的備援對象,找不到就返回nil。(缺點(diǎn):我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息。)
?(2.2) 完整的消息轉(zhuǎn)發(fā)
調(diào)用下列方法轉(zhuǎn)發(fā)消息:
-(void)forwardInvocation:(NSInvocation*)invocation
?NSInvocation把尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中,包括:選擇子、目標(biāo)及參數(shù)。
(a)上面這個方法可以實(shí)現(xiàn)的很簡單:只需改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用即可(與“備援接收者”方案所實(shí)現(xiàn)的方法等效,很少有人采用)。
(b)比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另外一個參數(shù),或是改換選擇子等等。
上面的步驟都不能解決問題的話,就會調(diào)用NSObject的doesNotRecognizeSelector拋出異常。
總結(jié):
消息轉(zhuǎn)發(fā)的全流程,如下圖所示:

“消息轉(zhuǎn)發(fā)”全流程圖
二、舉例
1. 動態(tài)方法解析,即resolveInstanceMethod的使用:
?。ㄒ詣討B(tài)方法解析來實(shí)現(xiàn)@dynamic屬性)
//EOCAutoDictionary.h
@interface EOCAutoDictionary : NSObject
@property(nonatomic, strong) NSDate *date;
@end
//EOCAutoDictionary.m
#import "EOCAutoDictionary.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"]) {
? ? ? ? // 向類中動態(tài)的添加方法,第三個參數(shù)為函數(shù)指針,指向待添加的方法。最后一個參數(shù)表示待添加方法的“類型編碼”
? ? ? ? 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 = [EOCAutoDictionarynew];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);
?2.?forwardingTargetForSelector的使用
注意:上面的resolveInstanceMethod返回YES的話,就無法調(diào)用forwardingTargetForSelector了。
下面的方法,對SLVForwardTarget的對象調(diào)用uppercaseString方法時,轉(zhuǎn)發(fā)給另一個對象"hello WorLD!"來執(zhí)行uppercaseString方法。
@implementation SLVForwardTarget
#pragma mark forwardingTargetForSelector
-(id) forwardingTargetForSelector:(SEL)aSelector {
? ? if(aSelector == @selector(uppercaseString)){
? ? ? ? return @"hello WorLD!";
? ? }
? ? return nil;
}
@end
3.?forwardInvocation的使用
?改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用的例子:
// 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;
}
系統(tǒng)check實(shí)例是否能response消息呢?如果實(shí)例本身就有相應(yīng)的response,那么就會響應(yīng)之,如果沒有系統(tǒng)就會發(fā)出methodSignatureForSelector消息,尋問它這個消息是否有效?有效就返回對應(yīng)的方法簽名,無效則返回nil。消息轉(zhuǎn)發(fā)機(jī)制使用從這個方法中獲取的信息來創(chuàng)建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。// Here, we ask the two real objects, realObject1 first, for their method 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:"改變目標(biāo)為mutableString類型,
"addObject:"和"objectAtIndex:"改變目標(biāo)為mutableArray類型。
三、unrecognized selector crash防護(hù)方案
根據(jù)上面的講解和舉例,我們知道,當(dāng)一個函數(shù)找不到時,runtime提供了三種方式去補(bǔ)救:
(1)調(diào)用resolveInstanceMethod給個機(jī)會讓類添加實(shí)現(xiàn)這個函數(shù);
(2)調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù);
(3)調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其它形式執(zhí)行。
第一種方案:
對于“unrecognized selector crash”,我們就可以利用消息轉(zhuǎn)發(fā)機(jī)制來進(jìn)行補(bǔ)救。對于使用上面三步中的哪一步來改造比較合適,我們選擇第二步forwardingTargetForSelector。初步分析原因如下:上面的三步接收者均有機(jī)會處理消息。步驟越往后,處理消息的代價就越大。forwardInvocation要通過NSInvocation來執(zhí)行函數(shù),得創(chuàng)建和處理完整的NSInvocation,開銷比較大。但resolveInstanceMethod給類添加不存在的方法,有可能這個方法并不需要,比較多余。用forwardingTargetForSelector將消息轉(zhuǎn)發(fā)給一個對象,開銷較小。
防護(hù)方案如下:
NSObject的類別NSObject+Forwarding來重寫forwardingTargetForSelector方法,讓執(zhí)行的目標(biāo)轉(zhuǎn)移到SLVUnrecognizedSelectorSolveObject里,然后SLVUnrecognizedSelectorSolveObject添加新的方法對未知選擇子進(jìn)行處理。在處理的這一塊兒,可以加上日志.
缺點(diǎn):
(1)類里的forwardingTargetForSelector如果提前返回nil了,就沒辦法執(zhí)行SLVStubProxy里的autoAddMethod方法。另外,未知選擇子對應(yīng)的類里面如果有forwardInvocation方法的話,會優(yōu)先執(zhí)行SLVStubProxy里的autoAddMethod方法,而不會執(zhí)行選擇子對應(yīng)的類里面的forwardInvocation方法。 整個處理流程,完全是按照以上三種方式的前后順序執(zhí)行,一旦一個方式解決了這個函數(shù)調(diào)用的問題,其它方法就不會執(zhí)行。這里得注意工程代碼里,可能就是需要自己的類里處理未知選擇子的情況。
?(2)還有一些selector如:"getServerAnswerForQuestion:reply:"、
"startArbitrationWithExpectedState:hostingPIDs:withSuppression:onConnected:"、
"_setTextColor:"、"setPresentationContextPrefersCancelActionShown:" ?也會攔截到。本來這些selector系統(tǒng)會自己處理的,相當(dāng)于這塊兒的攔截超前了,照這個比較大的缺陷來說,我們還是在第三步forwardInvocation來處理未知選擇子比較好,所以有了下面這個方案。
第二種方案:
消息轉(zhuǎn)發(fā)機(jī)制里的三個步驟處理未知選擇子,步驟越往后,處理消息的代價就越大。但是步驟越往前,我們越有可能攔截到系統(tǒng)的本來能處理的方法,這種方案是以犧牲效率來改善攔截的準(zhǔn)確性的。
防護(hù)方案如下:
NSObject的類別NSObject+Forwarding來重寫forwardInvocation方法,考慮到諸如"_navigationControllerContentInsetAdjustment"的選擇子有可能系統(tǒng)會在自己的forwardInvocation方法里進(jìn)行處理,所以此處先判斷系統(tǒng)的方法能否處理,系統(tǒng)的方法不能處理未知選擇子,再讓執(zhí)行的目標(biāo)轉(zhuǎn)移到未知選擇子處理對象SLVUnrecognizedSelectorSolveObject?里。然后SLVUnrecognizedSelectorSolveObject添加新的方法對未知選擇子進(jìn)行處理。在處理的這一塊兒,可以加上日志信息。
? ? ?以上兩種方案的代碼如下,其中用枚舉SLVUnrecognizedSelectorSolveScheme分別表示上面的兩種方案,可自行修改,這里推薦第二種方案:
// NSObject+Forwarding.m
#import "NSObject+Forwarding.h"
#import "SLVUnrecognizedSelectorSolveObject.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"
@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 {
? ? // 向類中動態(tài)的添加方法,第三個參數(shù)為函數(shù)指針,指向待添加的方法。最后一個參數(shù)表示待添加方法的“類型編碼”
? ? class_addMethod([self class], selector,(IMP)autoAddMethod,"v@:@");
? ? return YES;
}
id autoAddMethod(id self, SEL _cmd) {
? ? //可以在此加入日志信息,棧信息的獲取等,方便后面分析和改進(jìn)原來的代碼。
NSLog(@"unrecognized selector: %@",NSStringFromSelector(_cmd));??
?return 0;
}