在 iOS
项目中,我们经常会遇到 x[xx xx]: unrecognized selector sent to instance xxx
的 crash
,调用类没有实现的方法就会出现这个经典的 crash
,如下图,消息查找流程 这篇文章分析了如何找到报这个 crash
的原因,接下来我一步一步带你分析原因以及如何避免此 crash
。
一、动态方法决议
1._class_resolveMethod 分析
当调用类没有实现的方法时,先会去本类和父类等的方法列表中找该方法,若没有找到则会进入到动态方法决议 _class_resolveMethod,也是苹果爸爸给我们的一次防止 crash 的机会,让我们能有更多的动态性,那又该如何防止呢,接着往下看。
_class_resolveMethod(Class cls, SEL sel, id inst),当进行实例方法动态解析时,cls是类,inst是实例对象,如果是进行类方法动态解析时,cls是元类,inst是类。
if (resolver && !triedResolver) {
...
_class_resolveMethod(cls, sel, inst);
...
goto retry;
}
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 判断当前是否是元类
if (! cls->isMetaClass()) {
// 类,尝试找实例方法
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// 是元类,先找类方法
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 为什么这里还要查找一次呢?下面会分析
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
在这个方法会有两种情况,一种是对象方法决议,另外一种是类方法决议。
2.对象方法决议
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 看注释可以得知 SEL_resolveInstanceMethod 就是 类方法resolveInstanceMethod
// 去 cls 找是否实现了 resolveInstanceMethod 方法
// 如果没有实现,则直接返回,就不会给 cls 发送 resolveInstanceMethod 消息,就不会报找不到 resolveInstanceMethod
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
// 本类实现了类方法 resolveInstanceMethod
// 当对象找不到需要调用的方法时,系统就会主动响应 resolveInstanceMethod 方法,可以在 resolveInstanceMethod 进行自定义处理
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// 再次去查找方法,找不到就会崩溃
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 省略了一些不重要的报错信息代码
...
}
3._class_resolveInstanceMethod 小结
1.在 _class_resolveInstanceMethod 里首先会去本类查找类方法 resolveInstanceMethod 是否实现,如果本类没有实现则直接返回空,如果自己实现了就会走到下一步。
2.下一步会给本类发送 msg(cls, SEL_resolveInstanceMethod, sel) 消息,而本类却没有实现,但最终报的错不是找不到 resolveInstanceMethod 方法,所以有点奇怪,那是不是父类实现了呢?通过全局搜索 resolveInstanceMethod ,最终在 NSObject 里面找到这个方法的实现,所以会走到 NSObject 的实现返回 NO。
3.最后会通过 lookUpImpOrNil 再次去寻找该方法的实现,如果还没找到就会崩溃。
4.因为整个崩溃的原因是找不到方法实现,所以如果我们自己在本类里实现 resolveInstanceMethod,当没有找到方法实现最终会走到 resolveInstanceMethod 里面,在这个方法里面动态添加本类没有实现的 imp,最后一次的 lookUpImpOrNil 就会找到对应的 imp 进行返回,这样就不会导致项目的 crash 了。
5.resolveInstanceMethod 是系统给我们的一次机会,让我们可以针对没有实现的 sel 进行自定义操作。
解决方法如下
// 由于类方法和实例方法差不多,就写在一起了
// 实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"来了 老弟 - %p",sel);
if (sel == @selector(saySomething)) {
NSLog(@"说话了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(studentSayHello));
Method sayHMethod = class_getInstanceMethod(self, @selector(studentSayHello));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
// 类方法
// 类方法需要注意的一点是 类方法是存在元类里面的,所以添加的方法也是要添加到元类里面去
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"类方法 来了 老弟 - %p",sel);
if (sel == @selector(studentSayLove)) {
NSLog(@"说你爱我");
IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("Student"), @selector(studentSayObjc));
Method sayHMethod = class_getInstanceMethod(objc_getMetaClass("Student"), @selector(studentSayObjc));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(objc_getMetaClass("Student"), sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
3.类方法决议
_class_resolveClassMethod
和_class_resolveInstanceMethod
逻辑差不多,只不过类方法是去元类里处理。
/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
// 去元类里面找 resolveClassMethod,没有找到直接返回空
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
// 给类发送 resolveClassMethod 消息
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// _class_getNonMetaClass 对元类进行初始化准备,以及判断是否是根元类的一些判断,有兴趣的可以自己去看看
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// 再次去查找方法
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 省略了一些不重要的报错信息代码
...
}
4.类方法需要解析两次的分析
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 为什么这里还要查找一次呢?
_class_resolveInstanceMethod(cls, sel, inst);
}
既然上面的对象方法决议和类方法决议都会走 _class_resolveInstanceMethod,而最终都会找到父类 NSObject 里面去,那我们在 NSObject 分类里面重写 resolveInstanceMethod 方法,在这个方法里面对没有实现的方法(不管是类方法还是对象方法)进行动态添加 imp,然后再进行自定义处理(比如弹个框说网络不佳,在进行后台的bug收集),岂不是美滋滋了。
NSObject+crash.m
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"来了老弟:%s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(saySomething)) {
NSLog(@"说话了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayMaster));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
if (xx) {
// 后台 bug 收集或者其他一些自定义处理
}
}
二、消息转发
1.快速转发 forwardingTargetForSelector当自己没有进行动态方法决议时,就会来到我们的消息转发,那消息转发又是怎么样的呢?通过 instrumentObjcMessageSends(true); 函数来设置是否输出日志,且该日志存储在/tmp/msgSends-"xx";
Student *student = [[Student alloc] init];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
查看日志输出如下:
然后通过在源码中搜索 forwardingTargetForSelector 发现这个实现,好像没什么线索,那这个时候是不是就此就结束了?不,在源码中发现不了线索,我还有一个神器,官方文档 command + shift + 0,搜索 forwardingTargetForSelector,官方文档解释的清清楚楚明明白白。
Person.m
- (void)studentSaySomething {
NSLog(@"Person-%s",__func__);
}
Student.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(studentSaySomething)) {
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}
将 Student 未实现的方法在 Person 实现,然后 forwardingTargetForSelector 重定向到 Person 里,这样也不会造成崩溃。
2.慢速转发 methodSignatureForSelector
当我们在快速转发的 forwardingTargetForSelector 没有进行处理或者重定向的对象也没有处理,则会来到慢速转发的 methodSignatureForSelector。通过查看官方文档,methodSignatureForSelector 还要搭配 forwardInvocation 方法一起使用,具体的可以自行去官方文档查看。
methodSignatureForSelector:返回 sel 的方法签名,返回的签名是根据方法的参数来封装的。这个函数让重载方有机会抛出一个函数的签名,再由后面的 forwardInvocation 去执行。
forwardInvocation:可以将 NSInvocation 多次转发到多个对象。
Person.m
- (void)studentSaySomething {
NSLog(@"Person-%s",__func__);
}
Teacher.m
- (void)studentSaySomething {
NSLog(@"Person-%s",__func__);
}
Student.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"Student-%s",__func__);
// 判断selector是否为需要转发的,如果是则手动生成方法签名并返回。
if (aSelector == @selector(studentSaySomething)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"Student-%s",__func__);
// SEL aSelector = [anInvocation selector];
// if ([[Person new] respondsToSelector:aSelector])
// [anInvocation invokeWithTarget:[Person new]];
// else
// [super forwardInvocation:anInvocation];
// if ([[Teacher new] respondsToSelector:aSelector])
// [anInvocation invokeWithTarget:[Teacher new]];
// else
// [super forwardInvocation:anInvocation];
}
如果 forwardInvocation 什么都没做的话,仅仅只是 methodSignatureForSelector 返回了签名,则什么也不会发生,也不会崩溃。
慢速转发和快速转发比较类似,都是将A类的某个方法,转发到B 类的实现中去。不同的是,forwardInvocation 的转发相对更加灵活,forwardingTargetForSelector 只能固定的转发到一个对象,forwardInvocation 可以让我们转发到多个对象中去。
3.消息无法处理 doesNotRecognizeSelector
// 报出异常错误
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
三、总结
1.当动态方法决议resolveInstanceMethod
返回 NO
,就会来到 forwardingTargetForSelector:
,获取新的 target
作为receiver
重新执行 selector
,如果返回nil或者返回的对象没有处理,进入第二步。
2.methodSignatureForSelector
获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation
执行 NSInvocation
对象,并将结果返回。如果对象没有实现methodSignatureForSelector
,进入第三步。
3.doesNotRecognizeSelector:
抛出异常 unrecognized selector sent to instance %p
。
下面附上我总结的图