常见的Crash
unrecognized selector sent to class 是iOS编程中常见的错误,从之前
博文可知,iOS的方法调用最终会转化为消息发送过程
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
一般来说,runtime系统在进行消息发送过程中,如果找不到target的selector实现,runtime就会发送doesNotRecognizeSelector
消息,然后就会产生unrecognized selector sent to class
Crash错误。
如以下代码,对obj对象发送crashMethod方法的消息,正常来说,如果crashMethod方法没有实现,就会crash。
- (IBAction)btnAction:(id)sender {
CrashObject *obj = [CrashObject new];
[obj performSelector:@selector(crashMethod)];
}
报错
2018-11-02 14:40:04.347758+0800 CrashAvoid[34627:747177] +[CrashObject crash]: unrecognized selector sent to class 0x106636128
2018-11-02 14:40:04.375239+0800 CrashAvoid[34627:747177] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[CrashObject crash]: unrecognized selector sent to class 0x106636128'
*** First throw call stack:
(
0 CoreFoundation 0x00000001079b41bb __exceptionPreprocess + 331
1 libobjc.A.dylib 0x0000000106f52735 objc_exception_throw + 48
2 CoreFoundation 0x00000001079d2e44 +[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation 0x00000001079b8ed6 ___forwarding___ + 1446
4 CoreFoundation 0x00000001079bada8 _CF_forwarding_prep_0 + 120
5 CrashAvoid 0x0000000106632f2e -[ViewController btnAction:] + 62
6 UIKitCore 0x000000010a81becb -[UIApplication sendAction:to:from:forEvent:] + 83
7 UIKitCore 0x000000010a2570bd -[UIControl sendAction:to:forEvent:] + 67
8 UIKitCore 0x000000010a2573da -[UIControl _sendActionsForEvents:withEvent:] + 450
9 UIKitCore 0x000000010a25631e -[UIControl touchesEnded:withEvent:] + 583
10 UIKitCore 0x000000010a8570a4 -[UIWindow _sendTouchesForEvent:] + 2729
11 UIKitCore 0x000000010a8587a0 -[UIWindow sendEvent:] + 4080
12 UIKitCore 0x000000010a836394 -[UIApplication sendEvent:] + 352
13 UIKitCore 0x000000010a90b5a9 __dispatchPreprocessedEventFromEventQueue + 3054
14 UIKitCore 0x000000010a90e1cb __handleEventQueueInternal + 5948
15 CoreFoundation 0x0000000107919721 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
16 CoreFoundation 0x0000000107918f93 __CFRunLoopDoSources0 + 243
17 CoreFoundation 0x000000010791363f __CFRunLoopRun + 1263
18 CoreFoundation 0x0000000107912e11 CFRunLoopRunSpecific + 625
19 GraphicsServices 0x000000011008e1dd GSEventRunModal + 62
20 UIKitCore 0x000000010a81a81d UIApplicationMain + 140
21 CrashAvoid 0x0000000106633090 main + 112
22 libdyld.dylib 0x0000000109328575 start + 1
23 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
消息转发机制
然而,系统找不到target的selector时,到crash这段时间内还会有一些消息转发处理流程。实现这些流程可以避免crash的发生。
简单来说,按照消息转发流程的先后顺序可以分为以下三种消息转发流程。
- 动态添加方法实现 resolveInstanceMethod
- 把消息转发给另外一个对象 forwardingTargetForSelector
- 把消息转发给多个对象 forwardInvocation
流程如下图所示
resolveInstanceMethod
所谓动态添加方法实现的意思是说,你可以在消息转发流程动态为该对象创建一个方法实现。因为runtime通过objc_msgSend的方式,在类里面找不到方法的话,会启动消息转发流程,而第一步先去检查该类是否实现了resolveInstanceMethod或者resolveClassMethod方法(并同时添加了方法实现),如果添加成功后,就会由此动态添加的方法来响应这个消息。
如果需要在这个流程防护crash崩溃的话,可以直接添加一个NSobject的Category,复写resolveInstanceMethod(实例方法)和resolveClassMethod(类方法)方法,然后动态添加方法实现便可。
//UnrecognizedSelectorSolveObject.m
+ (BOOL)resolveInstanceMethod:(SEL)sel {
class_addMethod([self class], sel, (IMP)addMethod, "i@:");
return YES;
}
id addMethod(id self, SEL _cmd) {
NSLog(@"CrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return 0;
}
上面代码有个要注意的点
addMethod的这里的return 0要注意,对于OC来说,return 0代表返回nil,所以对一个nil发送消息时,就不会crash,不会要返回void
但是这样修改,编译器一般会报一个warning,
Category is implementing a method which will also be implemented by its primary class
也就是说,直接通过Category方式复写NSObject类方法,编译器是不推荐的,因为Category里重写的方法作用域是全局的,有可能会导致一些未知的错误。而且有一些第三方框架里面也有可能会重写这个方法,这样最终会被哪个Category执行是未知的。
forwardingTargetForSelector
如果上述动态添加方法没有被复写,消息转发流程便会走到第二步,严格来说,这一步才算是消息转发。你可以通过复写NSObject类方法,在这个方法里面返回一个中间类便可。意思就是由这个中间类来响应这个消息,一般来说,这个中间类不大可能有这个消息所对应的方法的实现,(除非你自己手动特定添加上去),所以还是要通过第一步所说的resolveInstanceMethod方法来动态添加方法通用地解决,最后消息还是会交给动态方法处理,避免crash。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return [UnrecognizedSelectorSolveObject new];
}
forwardInvocation
这一步的区别其实和上一步差别不大,上一步只能把消息转发到一个接收对象,而这一步通过NSInvocation对象可以转发给多个接收对象。
首先Runtime系统会调用methodSignatureForSelector
进行获取方法签名。如果返回 nil 说明消息无法处理并报错 unrecognized selector sent to instance,如果返回methodSignature,系统会生成一个NSInvocation对象,然后执行 forwardInvocation
,在这里可以通过修改NSInvocation对象来达到修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果依然不能正确响应消息,则报错 unrecognized selector sent to instance,当然,你可以可以在forwardInvocation
不做任何处理也是没关系的。
复写NSObject以下两个方法,然后在接收对象UnrecognizedSelectorSolveObject里面实现动态添加方法resolveInstanceMethod来避免crash。
关于NSInvocation的使用可以参考上一篇博文
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [NSMethodSignature methodSignatureForSelector:aSelector];
if (!sig) {
return [NSMethodSignature signatureWithObjCTypes:"i@:"];
}
return sig;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{//你也可以在这里不做任何处理
UnrecognizedSelectorSolveObject *target1 = [UnrecognizedSelectorSolveObject new];
UnrecognizedSelectorSolveXXXObject *target2 =
[UnrecognizedSelectorSolveXXXObject new];
[anInvocation invokeWithTarget:target1];
[anInvocation invokeWithTarget:target2];//转发给多个接收对象
}
最终crash防护方法
上述三种方法都可以达到防护unrecognized selector 】Crash的需求,但是直接使用并不是一个非常好的方式。
首先第一种方法,直接通过Category来复写NSObject类方法,这样对代码侵入过大,而第三种方法则需要管理方法签名和分发消息。所以说,选择第二种forwardingTargetForSelector会比较方便。
最终NSObject的Crash防护Category如下所示,可以通过设置白名单,过滤系统类来达到不必要的操作。
头文件UnrecognizedSelectorSolveObject.h
//
// NSObject+Safe.h
// CrashAvoid
//
// Created by conowen on 2018/11/1.
// Copyright © 2018 conowen. All rights reserved.
//
#import
NS_ASSUME_NONNULL_BEGIN
@interface UnrecognizedSelectorSolveObject : NSObject
@end
@interface NSObject (Safe)
@end
NS_ASSUME_NONNULL_END
源码文件UnrecognizedSelectorSolveObject.m
//
// NSObject+Safe.m
// CrashAvoid
//
// Created by conowen on 2018/11/1.
// Copyright © 2018 conowen. All rights reserved.
//
#import "NSObject+Safe.h"
#import
@interface UnrecognizedSelectorSolveObject ()
@end
@implementation UnrecognizedSelectorSolveObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
class_addMethod([self class], sel, (IMP)addResolveMethod, "i@:");
return YES;
}
id addResolveMethod(id self, SEL _cmd) {
NSLog(@"CrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return [NSNull null];
}
@end
@implementation NSObject (Safe)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[self swizzleMethod:@selector(forwardingTargetForSelector:) swizzledSelector:@selector(my_forwardingTargetForSelector:)];
}
});
}
- (id)my_forwardingTargetForSelector:(SEL)selector {
NSString *cls = NSStringFromClass(self.class);
// NSString *selectorStr = NSStringFromSelector(selector);
// 排除系统内部类与类方法
// if ([cls hasPrefix:@"_"] && [selectorStr hasPrefix:@"_"]) {
// return [self my_forwardingTargetForSelector:selector];
// }
if ([cls hasPrefix:@"PARS"]) {//白名单
//如果这个类本身就复写了forwardInvocation方法,跳过
//这里无需判断forwardingTargetForSelector,因为顺序的问题,先判断类本身,然后才到NSObject,如果类本身复写了forwardingTargetForSelector,就不会到这里
if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
if (imp != impOfNSObject) {
//NSLog(@"class has implemented invocation");
return nil;
}
}
return [UnrecognizedSelectorSolveObject new];
}else {
return [self my_forwardingTargetForSelector:selector];
}
}
#pragma mark - 方法交换
+ (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(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);
}
}
@end
多个Category问题
如果同时有多个NSObject的Category,例如都使用了第二种方法,复写forwardingTargetForSelector
,这时候按照Xcode的的编译规则,在Xcode里的buildPhases->Compile Sources
里面的从上至下顺序编译的,编译时通过压栈的方式将多个分类压栈,根据后进先出的原则,后编译的会被先调用,当消息传递时,找到方法并调用之后,就不再继续传递消息,所以还是依然调用最后一个加的方法。
所以多个NSObject的Category都复写了同一个方法的话,只会调用最后一个category的复写方法,这个顺序根据buildPhases->Compile Sources
的顺序来排序。
但是,如果对同一个
类方法,进行swizzleMethod操作的时候,而且siwzzle之后的方法名是不同的时候,最后一个category的swizzle 先会执行。其他category后执行。