美文网首页
【unrecognized selector 】Crash防护

【unrecognized selector 】Crash防护

作者: conowen | 来源:发表于2018-11-12 15:35 被阅读48次

    常见的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 <Foundation/Foundation.h>
    
    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 <objc/runtime.h>
    
    
    @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防护

          本文链接:https://www.haomeiwen.com/subject/qwqcxqtx.html