消息转发机制

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345