1、前言
在 iOS
项目中,我们经常会遇到 x[xx xx]: unrecognized selector sent to instance xxx
的 crash
,调用类没有实现的方法就会出现这个经典的 crash
,如下图,消息查找流程 这篇文章分析了如何找到报这个 crash
的原因,接下来我一步一步带你分析原因以及如何避免此 crash
。
2、动态方法决议
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 收集或者其他一些自定义处理
}
}
3、消息转发
1.快速转发 forwardingTargetForSelector
当自己没有进行动态方法决议时,就会来到我们的消息转发,那消息转发又是怎么样的呢?通过 instrumentObjcMessageSends(true);
函数来设置是否输出日志,且该日志存储在/tmp/msgSends-"xx"
;
Student *student = [[Student alloc] init];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
查看日志输出如下:
然后通过在源码中搜索forwardingTargetForSelector
发现这个实现,好像没什么线索,那这个时候是不是就此就结束了?不,在源码中发现不了线索,我还有一个神器,官方文档 command + shift + 0
,搜索 forwardingTargetForSelector
,官方文档解释的清清楚楚明明白白。
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
如果一个对象实现(或继承)此方法,并返回一个非nil
(和非self
)结果,则该返回的对象将用作新的接收者对象,消息分派将继续到该新对象。
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);
}
4、总结
- 1.当动态方法决议
resolveInstanceMethod
返回NO
,就会来到forwardingTargetForSelector:
,获取新的target
作为receiver
重新执行selector
,如果返回nil
或者返回的对象没有处理,进入第二步。 - 2.
methodSignatureForSelector
获取方法签名后,判断返回类型信息是否正确,再调用forwardInvocation
执行NSInvocation
对象,并将结果返回。如果对象没有实现methodSignatureForSelector,进入第三步。 - 3.
doesNotRecognizeSelector:
抛出异常unrecognized selector sent to instance %p
。 - 下面附上我总结的图