unrecognized selector 拦截2

498 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

如何选择拦截方案的建议

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

  • 动态方法解析 - 不建议

    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彻底说拜拜啦(手动微笑)。