iOS 面试题 - 什么时候会报 unrecognized selector 的异常?

159 阅读3分钟

什么时候会报 unrecognized selector 的异常?

函数 objc_msgSend()消息传递失败了,消息转发也失败,就会抛出这个异常。


image.png

首先在类对象或者元类对象中找不到 selector,去父类找也找不到

Runtime 的三次拯救机会也放弃:

动态方法决议Dynamic Method Resolution:

resolveInstanceMethod 或者 resolveClassMethod 中其实是可以给一个类对象添加 Method 的。

这个方法决议阶段能用来做什么我不知道,但是肯定不能用来做「crash 防护」。原因是:

respondsToSelector 会调用这个阶段的两个函数,而我们在这两个函数中给这个类添加方法(而且也不是真正的实现,是日志上报的实现)的话,就会对调用方造成误解:我问你你能不能做,你说能做。结果发现你说的能做是「能上报给其他人说你做不了」,你只是避免了我问你的时候的崩溃,但是没有改变你不能做的事实。这不合理啊兄弟.

所以「crash 防护」更合理的做法是放在「快速转发 fast forwarding」阶段实现。

+ (BOOL)resolveClassMethod:(SEL)sel {
    // 如果 dynamicClassMethod 实现了,那么不会跳到这里
    if(sel == @selector(dynamicClassMethod)) {
        Class class = objc_getMetaClass("MessageSendingDemo");
        IMP imp = class_getMethodImplementation(class, @selector(UploadMissingFunc));
        Method method = class_getInstanceMethod(class, @selector(UploadMissingFunc));
        const char *type = method_getTypeEncoding(method);
        
        return class_addMethod(class, sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}

+ (void)UploadMissingFunc {
    NSLog(@"就把未实现的方法换成了我了: %@", __func__,);
}

Fast Forwarding 快速转发

forwardingTargetForSelector: 中返回其他对象

使用场景:安全气囊,在函数 forwardingTargetForSelector 函数中添加 crash 上报逻辑,同时给这个未实现的 selector 创建一个空类(创建一个空对象)和空实现

- (id)forwardingTargetForSelector:(SEL)aSelector {
    id forwardTarget = [super forwardingTargetForSelector:aSelector];
    if (forwardTarget) {
        return forwardTarget;
    }
    
    Class someClass = [self qiResponedClassForSelector:aSelector];
    if (someClass) {
        forwardTarget = [someClass new];
    }
    // 另请高明
    return forwardTarget;
}

 unrecognized selector崩溃防护
代码来自:https://github.com/ParsifalC/NetEaseBaymaxDemo/blob/master/NetEaseBaymaxDemo/NSObject%2BBaymax.m
MARK: Unrecognize Selector Protected
- (id)baymax_forwardingTargetForSelector:(SEL)aSelector {
    // Ignore class which has overrided forwardInvocation method and System classes
    if ([self isMethodOverride:[self class] selector:@selector(forwardInvocation:)] ||
        ![NSObject isMainBundleClass:[self class]] ||
        [self isKindOfClass:[CPZombieObject class]]) {
        return [self baymax_forwardingTargetForSelector:aSelector];
    }
    
    // 上报 crash 日志
    NSLog(@"catch unrecognize selector crash %@ %@", self, NSStringFromSelector(aSelector));
    NSLog(@"%@", [NSThread callStackSymbols]);
    
    // 直接创建一个空类和空实现
    Class baymaxProtector = [NSObject addMethodToStubClass:aSelector];
    
    if (!self.baymax) {
        self.baymax = [baymaxProtector new];
    }
    // 就这么被防护掉了
    return self.baymax;
}

Normal Forwarding

methodSignatureForSelector: 函数中坚持不返回一个 NSMethodSignature,并且不在 forwardInvocation函数中把这个NSInvocation转发出去

快速转发只能转给一个对象,但是 Normal 转发可以转给很多个对象

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
   if (aSelector == @selector(work)) { 
        // 要执行 forwardInvocation ,必须返回函数签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
   SEL aSelector = [anInvocation selector];
   if ([[MyClass alloc] respondsToSelector:aSelector])
       // 交给 MyClass 这个类去执行了
       [anInvocation invokeWithTarget:[MyClass alloc]];
   else
       [super forwardInvocation:anInvocation];
}

这时你就获得了一个unrecognized selector 的异常

NSInvocation:用于在对象之间存储和转发消息,它包含了selector,target,参数和返回值,只需要给它一个正确的 target 对象,就能顺利执行一个函数(由target 对象执行的)。它可以被重复的分配到不同的 target。

NSMethodSignature:方法签名,主要是包含方法的返回值、参数的类型等信息