【unrecognized selector 】Crash防护

常见的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

流程如下图所示


ios-runtime-method-resolve.png

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的顺序来排序。

image.png

但是,如果对同一个类方法,进行swizzleMethod操作的时候,而且siwzzle之后的方法名是不同的时候,最后一个category的swizzle 先会执行。其他category后执行。

你可能感兴趣的:(【unrecognized selector 】Crash防护)