预防 app crash 之 unrecognized selector

曾无好事来相访,赖尔高文一起予。这篇文章主要讲述预防 app crash 之 unrecognized selector相关的知识,希望能为你提供帮助。
处理unrecognized selector异常原因假如封装一个方法,在其他模块调用该方法时,传入参数不匹配则crash。比如下面的方法:本应该传入的参数类型为NSMutableArray,如果传入的参数类型是NSArray,导致抛出  unrecognized selector异常

1 2 3 - (void)doSomethingWithArray:(NSMutableArray *)arr{ [arr addObject:@"123"]; }
当然,通过  参数类型判断  也可以避免问题的发生:
1 2 3 4 5 6 7 - (void)doSomethingWithArray:(NSMutableArray *)arr{ if ([arr isKindOfClass:[NSMutableArray class]]) { [arr addObject:@"123"]; }else{ CrashOnSimulator(@"??参数类型不对哦??"); } }
crash提醒:
1 2 3 void CrashOnSimulator(NSString *errorMsg) { if((TARGET_OS_SIMULATOR)){raise(SIGSTOP); } }
但是,有点地方可能忘记类型判断了怎么办,有全局拦截unrecognized selector  异常的方案吗?
分析 如何全局拦截unrecognized selector 异常
预防 app crash 之 unrecognized selector

文章图片

oc的消息发送机制咱们都熟悉了,通过superclass指针逐级向上查找该消息所对应的方法实现,如果遇到找不的方法,还有三次补救机制。我们可以通过上面三种方法中的一种,就可以避免unrecognized selector sent to instance
第一种方法:重写 NSObject 的forwardingTargetForSelector:
??filter unrecoginze seletor of intance only
思路
  • 创建一个接收未知消息的类,暂且称之为Protector
  • 创建一个NSObject  的分类,在分类中重写forwardingTargetForSelector: ,在这个方法中截获未实现的方法,转发给Protector。并为Protector  动态的添加未实现的方法,最后返回Protector  的实例对象。
  • 在分类中新增一个安全的方法实现,来作为Protector  接收到的未知消息的实现
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #import "NSObject+Protector.h" #import < objc/runtime.h> @implementation NSObject (Protector)   #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" - (id)forwardingTargetForSelector:(SEL)aSelector{   if ([self isCurrentClassInWhiteList]) { [[self class] warningDeveloper:aSelector];   Class protectorCls = NSClassFromString(@"ProtectorClassName"); if (!protectorCls){ protectorCls = objc_allocateClassPair([NSObject class], "ProtectorClassName", 0); objc_registerClassPair(protectorCls); }   if (![self isExistSelector:aSelector inClass:protectorCls]){ class_addMethod(protectorCls, aSelector, [self safeImplementation:aSelector],[NSStringFromSelector(aSelector) UTF8String]); }   Class Protector = [protectorCls class]; id instance = [[Protector alloc] init]; return instance; } else { return nil; } } #pragma clang diagnostic pop   - (BOOL)isCurrentClassInWhiteList{ NSArray *classNameArray = @[@"NSNull",@"NSString",@"NSArray",@"NSDictionary",@"NSURL"]; for (NSString *className in classNameArray) { if ([self isKindOfClass:NSClassFromString(className)]) { return YES; } } return NO; }   - (BOOL)isExistSelector: (SEL)aSelector inClass:(Class)currentClass{ BOOL isExist = NO; unsigned int methodCount = 0; Method *methods = class_copyMethodList(currentClass, & methodCount); for (int i = 0; i < methodCount; i++){ Method temp = methods[i]; SEL sel = method_getName(temp); NSString *methodName = NSStringFromSelector(sel); if ([methodName isEqualToString: NSStringFromSelector(aSelector)]){ isExist = YES; break; } } return isExist; }   - (IMP)safeImplementation:(SEL)aSelector{ IMP imp = imp_implementationWithBlock(^(){ NSLog(@"PROTECTOR: %@ Done", NSStringFromSelector(aSelector)); }); return imp; }   + (void)warningDeveloper:(SEL)aSelector{ #if DEBUG NSString *selectorStr = NSStringFromSelector(aSelector); NSLog(@"PROTECTOR: -[%@ %@]", [self class], selectorStr); NSLog(@"PROTECTOR: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self); NSLog(@"PROTECTOR: call stack: %@", [NSThread callStackSymbols]); // @throw @"方法找不到"; #endif }   @end
第二种方法:重写 NSObject 的methodSignatureForSelector(有些问题,下面有个最终版)
??filter unrecoginze seletor of class and intance
but 如果你使用了JSPatch、Aspects等对methodSignatureForSelector进行swizzle的第三方库,就别用这种方案了,有冲突,出现莫名的错误
思路
  • 创建NSObject+Protector重写methodSignatureForSelector,判断current class是否在白名单?
    • YES:返回一个空签名,啥也不做
    • NO:返回正常的签名,走原来的逻辑
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #import "NSObject+Protector.h" #import < objc/runtime.h> @implementation NSObject (Protector)   #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [[self class] __getMethodSignatureForSelector:aSelector]; }   - (void)forwardInvocation:(NSInvocation *)anInvocation{ } #pragma clang diagnostic pop   + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [self __getMethodSignatureForSelector:aSelector]; }   + (void)forwardInvocation:(NSInvocation *)anInvocation{ }   + (NSMethodSignature *)__getMethodSignatureForSelector:(SEL)aSelector{ if ([self isSubclassInWhiteListClass]) { [self warningDeveloper:aSelector]; return [NSMethodSignature signatureWithObjCTypes:"@"]; }else{ return [self instanceMethodSignatureForSelector:aSelector]; } }   + (BOOL)isSubclassInWhiteListClass{ NSArray *classNameArray = @[@"NSNull",@"NSString",@"NSArray",@"NSDictionary",@"NSURL"]; for (NSString *className in classNameArray) { if ([self isSubclassOfClass:NSClassFromString(className)]) { return YES; } } return NO; }   + (void)warningDeveloper:(SEL)aSelector{ #if DEBUG NSString *selectorStr = NSStringFromSelector(aSelector); NSLog(@"PROTECTOR: -[%@ %@]", [self class], selectorStr); NSLog(@"PROTECTOR: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self); NSLog(@"PROTECTOR: call stack: %@", [NSThread callStackSymbols]); //@throw @"方法找不到"; #endif }
第三种方法:最终方案(解决methodSignatureForSelector的不足)第三方库对 methodSignatureForSelector进行了全局替换,而我们也在NSObject中 进行了全局替换,冲突的点在于我们影响了第三库的自定义的Class。< br\>
  • 如何避免呢? 我们替换常用的的几个class就行了呗,是的,不过工作量有点大且重复,怎么办?用 define 来解决,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 //NSArray+WBGProtector.m   #import "NSArray+WBGProtector.h" #import < objc/runtime.h>   #define WBG_PROTECT_CLASS_NAME(_classname_)\ @implementation _classname_ (WBGProtector)\ \ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{\ return [[self class] __getMethodSignatureForSelector:aSelector type:@"instance"]; \ }\ \ - (void)forwardInvocation:(NSInvocation *)anInvocation{\ }\ \ + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{\ return [self __getMethodSignatureForSelector:aSelector type:@"class"]; \ }\ \ + (void)forwardInvocation:(NSInvocation *)anInvocation{\ }\ \ + (NSMethodSignature *)__getMethodSignatureForSelector:(SEL)aSelector type:(id)type{\ NSString *errorMsg = [NSString stringWithFormat:@"PROTECTOR: -[%@ %@],unrecognized selector sent to %@",self,NSStringFromSelector(aSelector),type]; \ NSLog(@"%@",errorMsg); \ CrashOnSimulator(errorMsg); \ return [NSMethodSignature signatureWithObjCTypes:"@"]; \ }\ \ @end\     WBG_PROTECT_CLASS_NAME(NSArray) WBG_PROTECT_CLASS_NAME(NSDictionary) WBG_PROTECT_CLASS_NAME(NSString)
细节分析 为什么需要白名单?
【预防 app crash 之 unrecognized selector】app启动加载一些系统方法,总是莫名其名的 报错 甚至crash
预防 app crash 之 unrecognized selector

文章图片

为什么去掉UIResponder?
isCurrentClassInWhiteList  中的classNameArray  本来是有  UIResponder的,但是后来测试发现UIWebView会出现异常!这里把UIResponder去掉了,毕竟过滤大部分的unrecognize selector主要的是NSArrayNSDictionary
预防 app crash 之 unrecognized selector

文章图片

测试code
1 2 3 4 5 6 7 // test Class method id clazz = [NSArray class]; [clazz viewDidLoad];   // test instance method NSMutableArray *arr = @{}; [arr addObject:@""]
遗留问题
    • 需要判断自己项目中引入的第三方库没有通过category的方式去重写NSObject的方法methodSignatureForSelector /forwardInvocation  以及forwardingTargetForSelector
      • 原因:一个category也不能可靠的覆盖另一个category中相同的类的相同的方法。例如UIViewController+A与UIViewController+B,都重写了viewDidLoad,我们就无法控制谁覆盖了谁。
    • 如果第三方重写了,则在这里通过swizzling的方式 替换 具体的实现方法




    推荐阅读