美文网首页iOS Crash拦截器
Crash拦截器 - 让unrecognized selecto

Crash拦截器 - 让unrecognized selecto

作者: 一纸苍白 | 来源:发表于2021-06-11 09:07 被阅读0次

    在本文中,我们将了解到如下内容:

    1. 基础的消息转发流程
    2. unrecognized selector 拦截建议
    3. 快速转发(Fast Forwarding)拦截unrecognized selector
    4. 常规转发(Normal Forwarding)拦截unrecognized selector

    前言

    我们在第一天学习Objective-C这一门语言的时候,就被告知这是一门动态语言。
    C这样的编译语言,在编译阶段就确定了所有函数的调用链,如果函数没有被实现,编译就根本不过了。而基于动态语言的特性,在编译期间,我们无法确认程序在运行时要调用哪一个函数,某一个未被实现的函数是否会在运行时被实现。
    这样就可能会出现运行时发现调用的函数根本不存在的尴尬,这也就是我们收到unrecognized selector sent to XXX这样的崩溃的原因了(动态语言也有让人心累的地方,手动叹气)。

    这篇文章要讨论的就是如果遇到了这种尴尬情况的时候,我们该如何避免我们最最最讨厌的崩溃(是的,所有的崩溃都是最最最让人讨厌的)。

    消息转发流程

    我们知道在我们调用某一个方法之后,最终调用的是objc_msgSend()这样一个方法,发送消息(selector)给消息接收者(receiver)。这个方法会根据OC的消息发送机制在receiver中查找selector。如果没有查找到,就会出现上述的运行时调用了未实现的函数的尴尬局面了。

    不过为了缓解这种尴尬,我们还有机会来挣扎。这挣扎机会就是消息转发流程

    消息转发流程包含以下3个步骤:

    1. 动态方法解析:resolveInstanceMethod:resolveClassMethod:
    2. 消息转发
      • 快速转发:forwardingTargetForSelector:
      • 常规转发:methodSignatureForSelector:forwardInvocation:

    消息转发流程是以动态方法解析消息快速转发消息常规转发这样的顺序来执行的。如果其中任意一个步骤能使消息被执行,那么就不会出现unrecognized selector sent to XXX的崩溃

    动态方法解析

    resolveInstanceMethod:这个方法的作用是动态地为selector提供一个实例方法的实现。而resolveClassMethod:则是提供一个类方法的实现。

    所以我们可以在这两个方法中,为对象添加方法的实现,再返回YES告诉已经为selector添加了实现。这样就会重新在对象上查找方法,找到我们新添加的方法后就直接调用。从而避免掉unrecognized selector sent to XXX

    需要注意的是: 这两个方法会响应respondsToSelector:instancesRespondToSelector:

    消息快速转发

    forwardingTargetForSelector:的作用是将消息转发给其它对象去处理。
    我们可以在这个方法中,返回一个对象,让这个对象来响应消息。

    需要注意的是: 如果在这个方法中返回selfnil,则表示没有可响应的目标。

    消息常规转发

    forwardInvocation:的作用也是将消息转发给其它对象。不过与 消息快速转发 不同的是该方法需要手动的创建一个NSInvocation对象,并手动地将新消息发送给新的接收者。

    很显然,这种方式会比 消息快速转发 付出更大的消耗。

    如何选择拦截方案的建议

    对于以上的三个步骤,我们该选择哪一个步骤来进行拦截呢?

    • 动态方法解析 - 不建议
      1. 这个方法会为类添加本身不存在的方法,绝大多数情况下,这个方法时冗余的。
      2. respondsToSelector:instancesRespondToSelector:这两个方法都会调用到resolveInstanceMethod:,那么在我们需要使用这两个方法进行判断的时候,就会出现我们不想看到的情况。
    • 消息快速转发 - 推荐
      会拦截掉已经通过消息常规转发实现的消息转发,但是可以通过判断避开对NSObject子类的消息常规转发的拦截。
    • 消息常规转发 - 推荐
      这一步不会对原有的消息转发机制产生影响,缺点是更大的性能开销。

    快速转发拦截方案

    我们可以创建一个例如:crashPreventor的类,在forwardingTargetForSelector:中为crashPreventor添加selector,最后返回crashPreventor的实例。从而让crashPreventor的实例响应这个selector。具体代码如下:

    
    @implementation NSObject (SelectorPreventor)
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        Class rootClass = NSObject.class;
        Class currentClass = self.class;
        return [self.class actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
    }
    
    + (id)forwardingTargetForSelector:(SEL)aSelector {
        Class rootClass = objc_getMetaClass(class_getName(NSObject.class));
        Class currentClass = objc_getMetaClass(class_getName(self.class));
        return [self actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
    }
    
    + (id)actionForwardingTargetForSelector:(SEL)aSelector rootClass:(Class)rootClass currentClass:(Class)currentClass {
        // 过滤掉内部对象
        NSString *className = NSStringFromClass(currentClass);
        if ([className hasPrefix:@"_"]) {
            return nil;
        }
    
        SEL methodSignatureSelector = @selector(methodSignatureForSelector:);
        IMP rootMethodSignatureMethod = class_getMethodImplementation(rootClass, methodSignatureSelector);
        IMP currentMethodSignatureMethod = class_getMethodImplementation(currentClass, methodSignatureSelector);
        if (rootMethodSignatureMethod != currentMethodSignatureMethod) {
            return nil;
        }
    
        NSString * selectorName = NSStringFromSelector(aSelector);
    
        // 上报异常
        // unrecognized selector sent to class XXX
        // unrecognized selector sent to instance XXX
        NSLog(@"unrecognized selector crash:%@:%@", className, selectorName);
    
        // 创建crashPreventor类
        NSString *targetClassName = @"crashPreventor";
        Class cls = NSClassFromString(targetClassName);
        if (!cls) {
            // 如果要注册类,则必须要先判断class是否已经存在,否则会产生崩溃
            // 如果不注册类,则可以重复创建class
            cls = objc_allocateClassPair(NSObject.class, targetClassName.UTF8String, 0);
            objc_registerClassPair(cls);
        }
    
        // 如果类没有对应的方法,则动态添加一个
        if (!class_getInstanceMethod(cls, aSelector)) {
            Method method = class_getInstanceMethod(currentClass, @selector(crashPreventor));
            class_addMethod(cls, aSelector, method_getImplementation(method), method_getTypeEncoding(method));
        }
    
        return [cls new];
    }
    
    #pragma clang diagnostic pop
    
    - (id)crashPreventor {
        return nil;
    }
    
    @end
    

    这里有几个点需要提一下:

    1. - (id)forwardingTargetForSelector:(SEL)aSelector;+ (id)forwardingTargetForSelector:(SEL)aSelector;都要在NSObject的分类中重写。前者对应实例方法,后者对应类方法。
    2. 过滤掉一些系统内部对象,否则在启动的时候就会有一些奇怪的异常被捕获到。
    3. 我们需要判断当前类是否实现了methodSignatureForSelector:方法,如果实现了该方法,就认为当前类已经实现了自己的消息转发机制,我们不对其进行拦截。
    4. 细心的我们肯定有注意到,不管是类方法还是实例方法,我们都是向crashPreventor中添加实例方法。这是因为,我们的响应对象时crashPreventor实例,而selector不区分实例方法还是类方法。我们这么处理最终对方法执行来说不会有什么差别。

    常规转发拦截方案

    实现比较简单,我们直接上代码:

    @implementation NSObject (SelectorPreventor)
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        return [NSMethodSignature signatureWithObjCTypes:"@"];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"forwardInvocation------");
    }
    
    + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        return [NSMethodSignature signatureWithObjCTypes:"@"];
    }
    
    + (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"forwardInvocation------");
    }
    
    #pragma clang diagnostic pop
    
    @end
    

    同样的,类方法和实例方法我们都需要重写。
    methodSignatureForSelector:中我们返回一个返回值为voidNSMethodSignature,在forwardInvocation:中我们不做任何事情。这样将性能消耗减到最小。

    以上:我们可以选择其中一种方式来实现我们对unrecognized selector的拦截,跟unrecognized selector彻底说拜拜啦(手动微笑)。

    相关文章

      网友评论

        本文标题:Crash拦截器 - 让unrecognized selecto

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