美文网首页
消息转发机制

消息转发机制

作者: 小溜子 | 来源:发表于2020-09-21 15:22 被阅读0次

iOS 项目中,我们经常会遇到 x[xx xx]: unrecognized selector sent to instance xxxcrash,调用类没有实现的方法就会出现这个经典的 crash,如下图,消息查找流程 这篇文章分析了如何找到报这个 crash 的原因,接下来我一步一步带你分析原因以及如何避免此 crash

image.png

一、动态方法决议

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);

查看日志输出如下:


image.png

然后通过在源码中搜索 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
下面附上我总结的图

image.png

相关文章

  • Runtime

    相关简单介绍 消息机制消息传递机制消息转发机制-动态添加方法消息转发机制-快速转发消息转发机制-慢速转发消息转发机...

  • runtime系列文章总结

    《iOS Runtime详解(消息机制,类元对象,缓存机制,消息转发)》《消息转发机制与Aspects源码解析》《...

  • iOS消息转发机制

    消息转发机制: 消息转发机制是相对于消息传递机制而言的。 1、消息(传递)机制 RunTime简称运行时。就是系统...

  • 《Effective Objective-C 2.0 》 阅读笔

    第12条:理解消息转发机制 1. 消息转发机制 当对象接收到无法解读的消息后,就会启动“消息转发”机制,开发者可经...

  • (十二) [OC高效系列]消息的派发机制

    1.什么是消息转发机制 消息转发机制是在调用未知方法时出现的 消息转发机制让程序员有机会去处理未知方法 消息转发机...

  • 消息转发机制(动态消息转发)

    例子分析 1)在给程序添加消息转发功能以前,必须覆盖两个方法,即methodSignatureForSelecto...

  • 深入理解Object-C消息转发机制

    深入理解Object-C消息转发机制 深入理解Object-C消息转发机制

  • 消息发送机制&消息转发机制

    消息发送机制&消息转发机制 消息发送机制:使用了运行时的方式, 通过SEL快速查找IMP的过程. 消息转发机制:I...

  • 消息转发机制

    为什么说 OC 的动态的 严格来说iOS中不存在方法调用的说法,应该说是消息的传递。消息传递和函数调用的区别就是,...

  • 消息转发机制

    前言 在上一篇Runtime源码 方法调用的过程中我们了解了消息的响应过程,即 先缓存查找,若未找到 接下来查找本...

网友评论

      本文标题:消息转发机制

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