前言:
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:以上代码自己写的,有待进一步检验和改进。