objc_msgSend流程分析之慢速查找

在快速查找流程中,如果缓存命中了还好说,那么如果命不中呢,就会到我们的objc_msgSend慢速查找流程,这篇文章就好好来分析是怎么进行慢速查找的

快速查找中如果没有找到方法实现,发现无论是CheckMiss还是JumpMiss,最终都会走到__objc_msgSend_uncached汇编函数,如图

CheckMiss和JumpMiss

然后我们command+F 搜索一下__objc_msgSend_uncached这玩意干了啥

__objc_msgSend_uncached汇编实现

找到之后,我们发现它做了一件事件,就是进行了方法列表的查找。然后我们接着搜索MethodTableLookup,找到其源码实现

MethodTableLookup汇编实现

前面的准备工作汇编我们不需要管,关键是bl _lookUpImpOrForward 这里跳转到_lookUpImpOrForward,然后搜索_lookUpImpOrForward发现找不到了,那么去哪里了呢?

接下来我把项目跑起来,通过断点来找到它

首先,在main函数中[person sayHello]处打一个断点,然后我们打开汇编调试,在顶部状态栏选择Debug -- Debug worlflow -- 勾选Always show Disassembly, 运行程序

汇编调试

运行完了之后,我们进到objc_msgSend里面去,按住control,点中间调试按钮(stepinto)就进去了

objc_msgSend里面

来到objc_msgSend里面后我们发现下面有一个_objc_msgSend_uncached,看到这里,果断打个断点,然后接着往里面进

_objc_msgSend_uncached里面

走进来终于找到了我们的lookUpImpOrForwardobjc-runtime-new.mm里面第6099行 ,这就很爽了,我们直接找过去就行了。这里补充一点,细心的朋友可能会发现下划线去哪里了, 这是因为从我们的汇编到C++会少一个下划线从C++到C会再少一个下划线,所以我们在汇编中去查找C/C++方法时要把下划线给去掉

明白了这一点之后我们接下来就全局搜索lookUpImpOrForward,就来到了objc-runtime-new.mm里面的lookUpImpOrForward, 这里面是通过C/C++写的,看起来还是要比汇编舒服一点

lookUpImpOrForward实现

还是老规矩,我把这里面的代码大致翻译一下,看我分析之前,大家也可以边打断点边看我注释,这样流程更加的清晰

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // 定义的消息转发
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // 快速查找,如果找到则直接返回imp
    // 这个地方为什么又进行了快速查找? 其目的是为了防止多线程操作时,刚好调用函数,就可以有缓存了
    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        // 快速查找在缓存里面找到了imp,直接返回imp
        if (imp) goto done_nolock;
    }

    // 加锁,目的是保证读取的线程安全
    runtimeLock.lock();

    
    // TODO: this check is quite costly during process startup.
    
    // 判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类
    checkIsKnownClass(cls);

    // 判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环
    // 这个地方没有imp,不是重点
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    // 判断类是否初始化,如果没有,需要先初始化
    // 这个地方没有imp,不是重点
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;
    
    // ------------重点在for循环里面----------------
    
    // unreasonableClassCount -- 表示类的迭代的上限
    //(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限的范围内,所以可以继续递归)
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        // 去当前类的方法列表查找(采用二分查找算法),如果找到,则goto done,将方法缓存到cache中
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        
        //  -------- 如果二分查找找自己没有找到,那么就开始找父类的缓存了 ----------
        // 注意, 这里将curClass = superclass 把父类赋值给了当前类了,并判断父类是否为nil
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            
            // 父类全部找完了之后,父类为nil了, 把imp赋值为forward_imp
            imp = forward_imp;
            // 赋值完了之后会退出本次循环,说明父类也没有这个方法
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        // 如果父类链中存在循环,则停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 找父类的缓存。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            
            // 如果在父类中找到了forward,则退出循环,调用此类的方法解析器
            break;
        }
        
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            // 如果在父类中,找到了此方法,将其存储到cache中
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    // 在自己和父类都没有找到方法实现,则会来到动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // 这里是此次动态方法决议的控制条件
        behavior ^= LOOKUP_RESOLVER;
        // 动态方法决议,给一次处理的机会
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    // 存储到缓存
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    // 解锁
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

这个代码流程分析的话我觉得要比上一篇文章的汇编要容易看懂一些。不过这里还是把整个慢速查找的流程总结一下

总结:

1、 首先cache缓存中快速查找没有找到,则进入慢速查找lookUpImpOrForward里面。来到这里之后还会进行快速查找一次,防止多线程操作时,刚好调用了此函数,这样就可以直接从缓存中快速查找, 不用再进行慢速查找了

2、判断cls, 是否是已知类。 然后该类是否实现,如果没有,则需要先实现,此时实例化的目的是为了确定父类链、ro、以及rw等,为后面的数据读取查找做好准备。

3、 来到for循环,慢速查找的重点流程 :

3.1: 去当前类的方法列表查找(采用二分查找算法),如果找到,则goto done,将方法缓存到cache中,如果没找到则开始找父类

3.2: 如果父类找到了imp,则直接返回imp,执行cache写入流程,如果循环完所有的父类还没找到,最终会找到nil,父类为nil的话就会走到imp = forward_imp,跳出当前循环

4、 当前类和父类都没找到imp, 就会来到动态方法决议,给一次机会重新进行查询,如果进行处理了,能拿到imp,就不会崩溃。

看完我这个总结,再去看我源码里的注释,我觉得应该非常清晰了,这就是整个方法的慢速查找流程。

在分析完objc_msgSend慢速查找之后,我再补充两个知识点

二分法算法

首先第一个知识点是查找方法列表的时候用的二分法查找,我们先来找到二分法算法的源码

二分法算法源码

这个代码我觉得非常的简单, 没有必要再一个个注释了,如果有看不懂的可以断点一试就明白了,也可以给我留言,我收到后会及时回复大家,我说一下大致的算法流程:

1、 拿到方法列表的第一个first和总数,然后开始遍历循环。注意,方法列表里面的数值是递增的,有序的

2、进到循环之后,probe = base + (count >> 1); 这句代码意思是首地址 位移 (count >> 1) 相当于从第一个元素移动到了中间

3、 移动到了中间之后,拿keyValue == probeValue 进行对比,如果等于,则返回method_t。注意,这里有一个 probe-- ,意思是排除分类里面名字一样的方法。

4、如果keyValue 大于 probeValue,即在(probe +1) 到 (count--)之间查找,同时count每循环一次,右移一位,双数减半,单数减半再减一,比如8右移一位是 8/2 = 4,7右移一位是 7/2 - 1 = 3。然后再回到第二个步骤进行查找

5、如果keyValue 小于 probeValue,即在1 - probe之间继续取中间位置进行查找,同时count每循环一次,右移一位。然后再回到第二个步骤进行查找

6、 一直循环到count0probeValue1 还没找到,就返回nil

消息转发

明白了二分法算法之后,我们最后再补充一个知识点,就是动态方法决议之后,会来到消息转发_objc_msgForward_impcache这里,这个是汇编实现的。

__objc_msgForward_impcache汇编

然后会走到__objc_msgForward,走到这里之后往下走,会来到__objc_forward_handler,我们搜索__objc_forward_handler找不到,因为汇编加了下划线,我们去掉一个下划线试试,搜索_objc_forward_handler,就找到了C++文件里面,然后最终走到了objc_defaultForwardHandler里面。这就是一直都没有找到实现的方法,崩溃时报的错误提示。

objc_defaultForwardHandler实现

最后,来一张消息转发机制流程图,为下一篇文章做准备

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