(2021 objc4-818源码分析)方法查找流程-动态决议&消息转发

方法查找流程

【第一步】 方法查找流程-快速查找流程
【第二步】 方法查找流程-慢速查找
【第三步】 动态决议
【第四步】 消息转发

动态决议

方法查找流程-慢速查找中,探索到如果通过慢速查找都没有找到,则会进行动态方法决议

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

resolveMethod_locked源码

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
动态方法决议图解.png
  • 如果没有找到就会调用resolveMethod_locked(inst, sel, cls, behavior)
  • 判断cls是否是元类
    • cls是元类
      • 执行resolveClassMethod(inst, sel, cls),使用类方法的动态方法决议
      • lookUpImpOrNilTryCache(inst, sel, cls),类方法解析是否找到或者为空
        • 如果没有找到或者为空,则执行resolveInstanceMethod(inst, sel, cls)
    • cls不是元类
      • 执行resolveInstanceMethod(inst, sel, cls),使用对象方法的动态方法决议

解决方法没有实现的思路一

通过上面的分析,我们知道如果没有找到方法的实现,会进行动态方法决议,那我们可以尝试针对类方法没有实现我们可以去实现resolveClassMethod,对象方法没有实现我们可以去实现resolveInstanceMethod

我们声明一个类,分别定义一个类方法和对象方法,不去实现它

+ (void)sayHello;
- (void)sayNB;
resolveClassMethod的实现

我们直接调用没有实现的类方法sayHello,运行发生崩溃,控制台打印结果如下:

+[Person sayHello]: unrecognized selector sent to class 0x100008208

解决方法:

+ (void)resolveClassMethodNotFound{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(sayHello)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
            
        IMP imp = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(resolveClassMethodNotFound));
        Method solveClassMethod  = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(resolveClassMethodNotFound));
        const char *type = method_getTypeEncoding(solveClassMethod);
        return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

再次运行的结果

sayHello 来了
+[Person resolveClassMethodNotFound]

分析
通过上面的代码,确实在慢速查找没有找打方法的实现的时候,类方法会来到resolveClassMethod,此时,我们实现resolveClassMethod,就能防止崩溃,这里的做法是把当前的方法,动态的添加一个类方法,并将sayHello的实现转移给新添加的方法

resolveInstanceMethod的实现

我们直接调用sayNB对象方法,会报方法找不到的报错如下:

-[Person sayNB]: unrecognized selector sent to instance 0x100442da0

会提示实例方法,找不到。unrecognized selector sent to instance

解决方案:

- (void)resolveinstanceMethodNotFound{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(self, @selector(resolveinstanceMethodNotFound));
        Method solveMethod  = class_getInstanceMethod(self, @selector(resolveinstanceMethodNotFound));
        const char *type = method_getTypeEncoding(solveMethod);
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

这里没有找到sayNB的方法实现,来到resolveInstanceMethod,只需动态的添加一个方法,即可视做完成了sayNB的实现。这里的做法是获取resolveinstanceMethodNotFoundimp,和签名。绑定到sayNBsel,我们知道方法的存储是通过sel & _mask来获取得到存储下标,这样我们在调用sayNB的时候,默认调用的就是resolveinstanceMethodNotFound的实现。

修改之后的结果

 sayNB 来了
 -[Person resolveinstanceMethodNotFound]

resolveMethod_locked的实现中,我们看到如下代码

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

对于类方法,再执行了resolveClassMethod之后,会尝试lookUpImpOrNilTryCache,从缓存中去找,如果没有找到,也会执行resolveInstanceMethod实例方法的动态决议,是不是意味着我们不用实现resolveClassMethod只实现一个resolveInstanceMethod就行了。因为元类没有实体类,沿着继承链我们只能找到NSObject,所以我们给NSObject添加一分类,并实现resolveInstanceMethod,来验证一下:

处理方案

  • 在NSObject+SolveInstance.h中添加如下代码,
  • 因为NSObject是根类,super为nil,所有我们这里直接返回NO。
  • 我们只处理明确知道的sayNBsayHello两个没有实现的方法,避免产生不必要的崩溃
  • 不管是类方法,还是实例方法,找到NSObject都会变成实例方法,所以这里返回转移的方法实现定义为实例方法就行了
- (void)resolveinstanceMethodNotFound{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(sayHello) || sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(self, @selector(resolveinstanceMethodNotFound));
        Method solveMethod  = class_getInstanceMethod(self, @selector(resolveinstanceMethodNotFound));
        const char *type = method_getTypeEncoding(solveMethod);
        return class_addMethod(self, sel, imp, type);
    }
    return NO;
}

跟我们预想的一样,打印结果如下

 sayHello 来了
 -[NSObject(SolveInstance) resolveinstanceMethod\M-LotFound]
 sayNB 来了
 -[NSObject(SolveInstance) resolveinstanceMethod\M-LotFound]

充分印证了:


isa与继承链.png

消息转发

打开消息发送的日志

我们跟log_and_fill_cache的源码的时候发现,消息的日志存储在/tmp/msgSends

消息发送日志存储.png

但是我们在这个目录下,并没有找到消息发送的日志,缺少了条件objcMsgLogFD == (-1),打开这个条件的方式如下:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [Person alloc];
        instrumentObjcMessageSends(YES);
        [person sayNB];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

这时我们就能拿到sayNB调用之后的消息日志了,不做任何崩溃处理,直接运行,在/tmp/msgSends目录下就获取到了消息日志

sayNB的消息调用日志.png

通过日志我们看到,再执行动态方法决议之后,还执行了forwardingTargetForSelectormethodSignatureForSelector
通过文档,我们还知道methodSignatureForSelector需要和forwardInvocation搭配起来使用。
接下来开始验证

消息转发-快速转发

forwardingTargetForSelector
在Person的.m文件中实现如下代码

实例方法-消息转发(快速转发)
/// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Student alloc];
}

// Student.m中的实现
- (void)sayNB {
    NSLog(@"%s", __func__);
}

打印结果如下

2021-01-18 15:38:14.107644+0800 消息转发流程[16585:199229] -[Person forwardingTargetForSelector:]- sayNB
2021-01-18 15:38:14.109524+0800 消息转发流程[16585:199229] -[Student sayNB]

实例方法可以转交给类方法么?代码如下

/// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Student class];
}

// Student.m中的实现
+ (void)sayNB {
    NSLog(@"%s", __func__);
}

打印如下:

2021-01-18 15:39:12.076271+0800 消息转发流程[16606:199968] -[Person forwardingTargetForSelector:]- sayNB
2021-01-18 15:39:12.077819+0800 消息转发流程[16606:199968] +[Student sayNB]

小结
对于实例方法,快速转发需要重写实例方法forwardingTargetForSelector

  • 如果转发的是一个,那么转发的类必须实现该消息的类方法实现

  • 如果是个对象,则需要实现该方法的实例方法实现
    如果以上不对应,或者没有实现,则会报错

  • 类方法-消息转发(快速转发)

/// 快速转发
+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Teacher class];
}

// Teacher.m的方法实现
+ (void)sayHello{
    NSLog(@"%s", __func__);
}

运行结果

2021-01-18 15:24:28.039137+0800 消息转发流程[16383:193004] +[Person forwardingTargetForSelector:]- sayHello
2021-01-18 15:24:28.040839+0800 消息转发流程[16383:193004] +[Teacher sayHello]

还可以有另外一种方式,代码如下

+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [Teacher alloc];
}
// Teacher.m的方法实现
- (void)sayHello{
    NSLog(@"%s", __func__);
}

运行结果

2021-01-18 15:31:07.617114+0800 消息转发流程[16474:195831] +[Person forwardingTargetForSelector:]- sayHello
2021-01-18 15:31:07.619068+0800 消息转发流程[16474:195831] -[Teacher sayHello]

小结
对于类方法,快速转发需要重写类方法forwardingTargetForSelector

  • 如果转发的是一个,那么转发的类必须实现该消息的类方法实现
  • 如果是个对象,则需要实现该方法的实例方法实现
    如果以上不对应,或者没有实现,则会报错

快速转发总结

  • 类方法和实例方法有自己对应的forwardingTargetForSelector实现,需要重写对应的forwardingTargetForSelector方法
  • 当前类或对象处理不了,可以交给其它类或者对象来处理。
  • 如果转交的是,则需要实现同名类方法
  • 如果转交的是对象,则需要实现同名的实例方法
  • 如果其它类或对象没有处理或上面两个规则不对应,就会报错,所转发的类没有找打改方法的实现
消息转发-慢速转发

methodSignatureForSelector
forwardInvocation
在Person.m 文件中写下如下代码

/// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s- %@", __func__, anInvocation);
}

运行,发现不报错,运行结果如下

2021-01-18 16:43:19.517884+0800 消息转发流程[17265:220490] -[Person methodSignatureForSelector:]- sayNB
2021-01-18 16:43:19.520205+0800 消息转发流程[17265:220490] -[Person forwardInvocation:]- <NSInvocation: 0x1004078a0>

forwardInvocation中加上如下代码

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s- %@", __func__, anInvocation);
    [anInvocation invokeWithTarget:[Student alloc]];
}

打印结果

2021-01-18 16:46:21.053838+0800 消息转发流程[17325:222827] -[Person methodSignatureForSelector:]- sayNB
2021-01-18 16:46:21.059838+0800 消息转发流程[17325:222827] -[Person forwardInvocation:]- <NSInvocation: 0x100606dd0>
2021-01-18 16:46:21.061812+0800 消息转发流程[17325:222827] -[Student sayNB]

同样anInvocationtarget可以是一个类,当需要实现同名的类方法
总结

  • methodSignatureForSelector需要搭配forwardInvocation使用,forwardInvocation系统会自动调用。
  • forwardInvocation可以不处理anInvocation事物,不会引发崩溃
  • anInvocationtarget可以是,也可以是实例对象,若指定了或者对象,必须实现对应的同名类方法或实例方法

我们将,动态方法决议,消息转发-快速转发,消息转发-慢速转发都放开,只在消息转发-慢速转发中处理,代码如下

/// 动态方法决议
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

/// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

/// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s- %@", __func__, anInvocation);
    [anInvocation invokeWithTarget:[Student alloc]];
}

得出如下结果:

2021-01-18 17:27:22.544241+0800 消息转发流程[17843:242496] +[Person resolveInstanceMethod:] - sayNB
2021-01-18 17:27:22.546082+0800 消息转发流程[17843:242496] -[Person forwardingTargetForSelector:] - sayNB
2021-01-18 17:27:22.546903+0800 消息转发流程[17843:242496] -[Person methodSignatureForSelector:]- sayNB
2021-01-18 17:27:22.547638+0800 消息转发流程[17843:242496] +[Person resolveInstanceMethod:] - _forwardStackInvocation:
2021-01-18 17:27:22.549293+0800 消息转发流程[17843:242496] -[Person forwardInvocation:]- <NSInvocation: 0x100604450>
2021-01-18 17:27:22.549812+0800 消息转发流程[17843:242496] -[Student sayNB]

我们再去消息日志里面查看,得到的调用顺序同样是如上面所示。


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

推荐阅读更多精彩内容