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。
消息转发
我们知道一个方法没有在类结构体中或是集成链中找到,会触发消息转发流程,这个流程分三步
- 动态方法解析: Method Resolution
- 快速转发: Fast Rorwarding
- normal forwarding
动态方法解析: Method Resolution
这一步有两个方法+ (BOOL)resolveInstanceMethod:
或者+ (BOOL)resolveClassMethod:
,第一个是针对实例方法,第二个是针对类方法。在这个方法中可以通过class_addMethod
动态增加SEL
和IMP
关系,也就是可以为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问题。
网友评论