iOS 带你走进消息转发流程及防崩溃处理

4,434 阅读7分钟

1、前言

iOS 项目中,我们经常会遇到 x[xx xx]: unrecognized selector sent to instance xxxcrash,调用类没有实现的方法就会出现这个经典的 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
  • 下面附上我总结的图