美文网首页
UnrecognizedSelectorCrash

UnrecognizedSelectorCrash

作者: tom__zhu | 来源:发表于2022-01-23 22:08 被阅读0次

iOS消息发送机制包含以下几个步骤:
1、检查selector是否要要忽略,例如arc 下调用release、retain
2、检查target是否是nil,如果是nil后续流程不再进行
3、从objc_class -> cache(struct cache_t) -> _buckets(struct bucket_t) 获取方法缓存,有缓存执行,没有缓存执行步骤4
4、从类中的方法列表中查找objc_class -> method_array_t(struct class_rw_t),有方法就执行,没有就沿着集成链去父类中查找直至找到NSObject。有找到就执行,没有找到就执行消息转发流程。

OC方法解析

OC方法最终会通过objc_msgSend发送,这个方法是个可变参方法,第一个参数是self,第二个是op,也就是selector,第三个是...也就是变参。
其实OC方法本身是隐藏了两个参数,一个是id类型的self,另一个是SEL类型的_cmd。

消息转发

我们知道一个方法没有在类结构体中或是集成链中找到,会触发消息转发流程,这个流程分三步

  1. 动态方法解析: Method Resolution
  2. 快速转发: Fast Rorwarding
  3. normal forwarding

动态方法解析: Method Resolution

这一步有两个方法+ (BOOL)resolveInstanceMethod:或者+ (BOOL)resolveClassMethod:,第一个是针对实例方法,第二个是针对类方法。在这个方法中可以通过class_addMethod动态增加SELIMP关系,也就是可以为SEL增加方法实现。

void foo(void) {
    
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo_int:)) {
        class_addMethod([self class], sel, (IMP)foo, "c@:");
        return true;
    }
    return false;
}

class_addMethod参数有四个,最后一个是method type类型,可以参考苹果文档#

快速转发: Fast Rorwarding

- (id)forwardingTargetForSelector:(SEL)aSelector通过这个方法可以将selector转发给可以响应的对象。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo_int:)) {
        return [MyObjc new];
    }else {
        ......
    }
}

如果返回nil或是不重写这个方法,会执行第三步。

完整消息转发: Normal Forwarding

之所以说这一步是完整消息转发,是因为相比前面两步只能操作selector,这里还能操作target。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    if (sel == @selector(foo_int:)) {
        [anInvocation invokeWithTarget:[MyObjc new]];
    }else {
        ...
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
}

-methodSignatureForSelector:方法返回一个NSMethodSignature,根据这个内容可以生成NSInvocation实例对象并执行- forwardInvocation:,在这里可以讲消息转发出去,或是只是简单记录上报,为后续排查问题提供线索。

如何处理UnrecognizedSelectorCrash方法

NSObject创建分类,把系统实例方法forwardInvocation``methodSignatureForSelector以及系统类方法methodSignatureForSelector``forwardInvocation与自己的方法交换

+ (void)start {
    // Instance Method
    swizzleInstanceMethod([self class], @selector(forwardInvocation:), @selector(my_forwardInvocation:));
    swizzleInstanceMethod([self class], @selector(methodSignatureForSelector:), @selector(my_methodSignatureForSelector:));
    
    // Class Method
    swizzleClassMethod([self class], @selector(methodSignatureForSelector:), @selector(my_classMethodSignatureForSelector:));
    swizzleClassMethod([self class], @selector(forwardInvocation:), @selector(my_classforwardInvocation:));
}

+ (NSMethodSignature *)checkObjectSignatureAndForwardInvocation:(Class)currentClass Selector:(SEL)aSelector {
    IMP methodSignatureIMPOfNSObject = class_getMethodImplementation([NSObject class], @selector(methodSignatureForSelector:));
    IMP methodSignatureIMPOfCurrentClass = class_getMethodImplementation(currentClass, @selector(methodSignatureForSelector:));
    // If current class override methodSignatureForSelector return nil
    if (methodSignatureIMPOfNSObject != methodSignatureIMPOfCurrentClass) {
        return nil;
    }
    
    IMP forwardInvocationIMPOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
    IMP forwardInvocationIMPOfCurrentClass = class_getMethodImplementation(currentClass, @selector(forwardInvocation:));
    // If current class only override forwardInvocation, print log
    if (forwardInvocationIMPOfNSObject != forwardInvocationIMPOfCurrentClass) {
        NSString *message = [NSString stringWithFormat:@"Unrecognized instance class:%@ and selector:%@", NSStringFromClass(currentClass), NSStringFromSelector(aSelector)];
        handleCrashException(QYCrashProtectionUnrecognizedSelector, message);
    }

    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}


- (void)my_forwardInvocation:(NSInvocation*)invocation {
    
}

- (NSMethodSignature *)my_methodSignatureForSelector:(SEL)sel {
    return [[self class] checkObjectSignatureAndForwardInvocation:[self class] Selector:sel];
}


- (NSMethodSignature *)my_classMethodSignatureForSelector:(SEL)sel {
    return [[self class] checkObjectSignatureAndForwardInvocation:[self class] Selector:sel];
}

- (void)my_classforwardInvocation:(NSInvocation*)invocation {
    
}

结论

既然有三个步骤可以拦截UnrecognizedSelectorCrash问题,那么应该在哪里操作呢。通常会在forwardInvocation方法拦截,因为前两种方法提供的信息有限,只有一个selector信息。我们并不能提前知道哪些方法会发生闪退,而且应该向哪个类增加方法,以及向哪个类转发消息,都不知道。所有另外有些系统函数也会走到forwardInvocation方法,如果这里统一处理,会导致进程闪退。所以通常在forwardInvocation:``methodSignatureForSelector:方法统一处理UnrecognizedSelectorCrash问题。

相关文章

  • UnrecognizedSelectorCrash

    iOS消息发送机制包含以下几个步骤:1、检查selector是否要要忽略,例如arc 下调用release、ret...

网友评论

      本文标题:UnrecognizedSelectorCrash

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