iOS中对unrecognized selector的防御

作者: Mad_Mark | 来源:发表于2017-02-09 21:34 被阅读1616次

    在 iOS 开发中,App的崩溃原因有很多种,这篇文章主要阐述我所使用的防止发送未知消息(unrecognized selector)**导致崩溃的方法及思路,希望能起到抛砖引玉的作用。若有错误,欢迎指出!

    unrecognized selector sent to instance 0x7faa2a132c0

    调试过程中如果看到输出这句话,我们马上就能知道某个对象并没有实现向他发送的消息。如果是在已经上线的版本中发现的……GAME OVER...(你也可以用热修复)

    消息发送的机制我们都明白,通过superclass指针逐级向上查找该消息所对应的方法实现。如果直到根类都没有找到这个方法的实现,运行时会通过补救机制,继续尝试查找方法的实现。那么我们能不能通过重写其中的某个方法,来达到不崩溃的目的?

    我们先了解下这个补救机制:

    runtime_sendMsg.png

    直到最后一步消息无法处理后,我们的App就崩溃了,随后我们就看到了熟悉的unrecognized selector...
    这些方法究竟能做什么,我们来看看苹果官方的描述(我对其中比较重要的部分翻译了一下):

    resolveInstanceMethod:

    resolveInstanceMethod:resolveClassMethod: 方法允许你为一个给定的 selector 动态的提供方法的实现。
    OC 方法在底层的C函数的实现中需要至少两个参数:self 和 _cmd。使用** class_addMethod **函数,你能够添加一个函数到一个类来作为方法使用。

    ** forwardingTargetForSelector:**

    如果一个对象实现了这个方法,并且返回了一个非空(以及非 self)的结果,返回的对象会用来作为一个新的接收对象,随后消息会被重新派发给这个新对象。(很明显,如果你在这个方法中返回了self,那这段代码将会坠入无限循环。)
    如果你这段方法在一个非 root 的类中实现,并且如果这个类根据给定的selector什么都不作返回,那么你应该返回一个 执行父类的实现后返回的结果。

    这个方法为对象在开销大的多的 forwardInvocation: 方法接管之前提供了一次转发未知消息的机会。这对你只是想简单的重新定位消息到另一个对象是非常有用的,并且相对普通转发更快一个数量级。如果转发的目的是捕捉到NSInvocation,或者操作参数,亦或者是在转发过程中返回一个值,那这个方法就没有用了。

    ** forwardInvocation: **

    当对象接受到一条自己不能响应的消息时,运行时会给接收者一次机会来把消息委托给另一个接收者。他委托的消息是通过NSInvocation对象来表示的,然后将这个对象作为** forwardInvocation: 的参数。接收者收到 forwardInvocation: **这条消息后可以选择转发这个NSInvacation对象给其他接收对象。(如果这个接收对象也不能响应这条消息,他也会给一次转发这条消息的机会。)

    因此 forwardInvocation: 允许在两个对象之间通过某个消息来建立关系。转发给其他对象的这种行为,从某种意义上来说,他“继承”了他所转发给的对象的一些特征。

    注意
    为了响应这个你无法识别的方法,你除了 forwardInvocation: 方法外,还必须重写 methodSignatureForSelector: ** 方法。在转发消息的机制中会从 methodSignatureForSelector: **方法来创建NSInvocation对象。所以你必须为给定的 selector 提供一个合适的 method signature ,可以通过预先设置一个或者向另一个对象请求一个。

    以上,是苹果官方文档对这三个关键方法的解释。

    简而言之:

    • **resolveInstanceMethod: ** 会为对象或类新增一个方法。如果此时这个类是个系统原生的类,比如 NSArray ,你向他发送了一条 setValue: forKey: 的方法,这本身就是一次错发。此时如果你为他添加这个方法,这个方法一般来说就是冗余的。

    • ** forwardInvocation: ** 必须要经过 methodSignatureForSelector: ** 方法来获得一个NSInvocation,开销比较大。苹果在 forwardingTargetForSelector **的discussion中也说这个方法是一个相对开销多的多的方法。

    • ** forwardingTargetForSelector: ** 这个方法目的单纯,就是转发给另一个对象,别的他什么都不干,相对以上两个方法,更适合重写。

    既然** forwardingTargetForSelector: **方法能够转发给别其他对象,那我们可以创建一个类,所有的没查找到的方法全部转发给这个类,由他来动态的实现。而这个类中应该有一个安全的实现方法来动态的代替原方法的实现。

    整理下思路:

    1. 创建一个接收未知消息的类,暂且称之为 Protector
    2. 创建一个 NSObject 的分类
    3. 在分类中重写** forwardingTargetForSelector: **,在这个方法中截获未实现的方法,转发给 Protector。并为 Protector 动态的添加未实现的方法,最后返回 Protector 的实例对象。
    4. 在分类中新增一个安全的方法实现,来作为 Protector 接收到的未知消息的实现

    上代码:

    创建一个Protector类,没必要new文件出来,动态生成一个就可以了。注意,如果这个方法被执行到两次,连续两次创建同一个类一定会崩溃,所以我们要加一层判断:

    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        
        Class protectorCls = NSClassFromString(@"Protector");
        if (!protectorCls)
        {
            protectorCls = objc_allocateClassPair([NSObject class], "Protector", 0);
            objc_registerClassPair(protectorCls);
        }
    }
    

    然后我们要为这个类添加方法,在添加方法之前我们也要做一层判断,是否已经添加过这个方法(此处文末有更新说明)

            NSString *selectorStr = NSStringFromSelector(aSelector);
            // 检查类中是否存在该方法,不存在则添加
            if (![self isExistSelector:aSelector inClass:protectorCls])
            {
                class_addMethod(protectorCls, aSelector, [self safeImplementation:aSelector],
                                [selectorStr UTF8String]);
            }
    

    这里面有一个** safeImplementation: **方法,其实就是生成一个IMP,然后返回。这里我只是简单的输出一句话:

    // 一个安全的方法实现
    - (IMP)safeImplementation:(SEL)aSelector
    {
        IMP imp = imp_implementationWithBlock(^()
        {
            NSLog(@"PROTECTOR: %@ Done", NSStringFromSelector(aSelector));
        });
        return imp;
    }
    

    isExistSelector: inClass:的实现代码如下,主要是根据给定的selector在class中查找,如果找到对应的实现则返回YES:

    // 判断某个class中是否存在某个SEL
    - (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;
    }
    

    回到我们的** forwardingTargetForSelector: **方法,接下来就该返回Protector的实例了:

            Class Protector = [protectorCls class];
            id instance = [[Protector alloc] init];
            
            return instance;
    

    但是经过测试,目前的代码还有个问题:App启动时有些系统方法也会经由这个方法转发对象,启动完成就不存在这种问题。所以我们在** forwardingTargetForSelector: **方法中要再加一次判断,如果 self 是我们所关心的类,我们才转发对象,否则返回nil。
    以下是 **forwardTargetForSelector: **完整的代码,这里我关心的是UIResponder 和 NSNull这两个类(你也可以添加诸如NSArray\NSDictionary等类):

    // 重写消息转发方法
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        NSString *selectorStr = NSStringFromSelector(aSelector);
        // 做一次类的判断,只对 UIResponder 和 NSNull 有效
        if ([[self class] isSubclassOfClass: NSClassFromString(@"UIResponder")] ||
            [self isKindOfClass: [NSNull class]])
        {
            NSLog(@"PROTECTOR: -[%@ %@]", [self class], selectorStr);
            NSLog(@"PROTECTOR: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self);
            // 查看调用栈
            NSLog(@"PROTECTOR: call stack: %@", [NSThread callStackSymbols]);
    
            // 对保护器插入该方法的实现
            Class protectorCls = NSClassFromString(@"Protector");
            if (!protectorCls)
            {
                protectorCls = objc_allocateClassPair([NSObject class], "Protector", 0);
                objc_registerClassPair(protectorCls);
            }
            
            // 检查类中是否存在该方法,不存在则添加
            if (![self isExistSelector:aSelector inClass:protectorCls])
            {
                class_addMethod(protectorCls, aSelector, [self safeImplementation:aSelector],
                                [selectorStr UTF8String]);
            }
            
            Class Protector = [protectorCls class];
            id instance = [[Protector alloc] init];
            
            return instance;
        }
        else
        {
            return nil;
        }
    }
    

    以上就是所有代码(所以我就不上传DEMO了)。

    实验结果:

    试验中,我对一个label perform了一个未知的方法:callMeTryTry,由于他是一个UIRespnder的子类,所以会进入调用我们的 Protector。控制台输出如下,并且没有崩溃。(所有日志不是真的崩溃时候的日志,前面都带有 PROTECTOR 字样,全都是我代码里的输出),你也可以不进行类的判断试一下,你会看到很多这样的输出。

    console_log.png

    以上就是本文全部,希望对各位有帮助,有问题也可以互相交流。

    20170214 更新:
    class_addMethod 方法之前,其实不需要判断是否已添加过这个方法。因为苹果官方文档说 class_addMethod 方法只会覆盖父类的方法,或者不存在的方法。如果是已经存在的方法,他不会重复添加或替代。
    所以** - (BOOL)isExistSelector: (SEL)aSelector inClass:(Class)currentClass **可以不要了。

    相关文章

      网友评论

        本文标题:iOS中对unrecognized selector的防御

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