OC底层原理十三: objc_msgSend(方法慢速查找)

OC底层原理 学习大纲

梳理下调用方法的流程,避免大家迷路。

  • 我们对象(实例对象或类)调用方法,都是执行objc_msgSend:
  • step1: 进入汇编语言,在cache快速查找,找到了返回imp,没找到走step2
  • step2: 进入c/c++底层,在methodList中查找,(会将方法写入缓存,保障后续调用时,能直接在第一步就获取到imp),找到了的返回imp,没找到走step3
  • step3: 走最后处理机制(三重防护,这个后面详细介绍),没找到走step4
  • step4: 执行默认的imp,报错提示, crash

上一节已了解方法在cache中的高速查找流程,当cache中找不到imp时,我们进入step2

方法慢速查找流程

请跟着我,一步步细致理解。我相信你会收货满满的干货

  1. 了解高速cache转低速c/c++底层过程
  2. 初始值forward_imp
  3. 慢速前的cache读取
  4. 检查类是否合法
  5. 类的初始化(双向链表)
  6. 当前类和父类循环查找imp
  7. 最后的防线
  8. 查找结果

1. 了解高速cache转低速c/c++底层过程

  • 我们选择arm64真机环境,在objc4源码中进行探索。

  • 上一节我们在缓存找不到imp后,就来到了__objc_msgSend_uncached->MethodTableLookup方法列表查找 -> _lookUpImpOrForward

  • objc4源码中搜索_lookUpImpOrForward,发现没有具体实现。 取消_,搜索lookUpImpOrForward

总结.png
  • 发现lookUpImpOrForward不再是汇编语言,而是oc底层语言。这也是为什么说从这开始,就是从快速搜索(汇编)回到慢速搜索(c/c++)

代码检验流程

main.m中加入测试代码。 在[p sayHello];加入断点

@interface HTPerson : NSObject
- (void)sayHello;
@end

@implementation HTPerson
- (void)sayHello{
   NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
   @autoreleasepool {
       HTPerson *p  = [HTPerson alloc];
       [p sayHello];
   }
   return 0;
}
代码检验.jpg

2. 初始值forward_imp

lookUpImpOrForward函数第一步,设置初始变量(forward_imp、imp、curClass)

    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

我们先弄清楚forward_imp是什么?

  • 全局搜索_objc_msgForward_impcache
image.png

注:
1、C/C++调用汇编,去汇编中查找时: 在方法前一个下划线
2、汇编调用C/C++方法,去C/C++中查找时: 在方法一个下划线

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

结论: forward_imp 的值就是未找到imp的函数实现

3. 慢速前的cache读取

在进入lookUpImpOrForward函数前,我们在汇编MethodTableLookup宏中了解behavior的值为3

image.png

lookUpImpOrForward函数继续往下看:

    if (fastpath(behavior & LOOKUP_CACHE)) {  
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

因为LOOKUP_CACHE = 4behavior = 3,二进制得出 behavior & LOOKUP_CACHE0

  • if判断条件不成立

初次进入lookUpImpOrForward是从缓存进入的,所以没必要再在缓存中进行查询,但是后续再次进入时,控制入参behavior的值,会触发cache_getImp缓存查询

我们看看cache_getImp,发现又回到了高速汇编层,全局搜索_cache_getImp:

image.png

发现调用了CacheLookup,继续在clscache高速查找当前sel对应的imp

CacheLookup高速查找流程在上一节已详细介绍

主要目的:

  • 可能多个线程都在执行objc_msgSend任务,在你第一次高速查找未找到时,可能其他线程已将cls的sel和imp写入了cls的缓存

  • 高速查找(汇编)低速查找(c/c++)性能太多了,我们进入慢速查找前,我们再走一次高速缓存查找。 (缓存读取不到再过来lookUpImpOrForward的,默认走了一遍高速缓存了,就没再进入了)

  • 如果获取到imp,就跳转到done_nolock(下面一起分析), 如果没有,进入慢速查找流程。

4. 检查类是否合法

 checkIsKnownClass(cls);

确保cls已知类列表中,避免传入一个非类二进制文件,进行CFI攻击

5. 类的初始化(双向链表)

确保cls已存在,将cls赋值给curClass

  • 如果不存在,就现在创建类( 因为swiftOC类的结构不一样,所以需要单独判断是否存在。)
   // 如果类没有实现或内部信息不完整
    if (slowpath(!cls->isRealized())) {
        // 实现类(swift和oc)  递归实现完整的继承链和isa指向链
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    // 如果类没有初始化
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        // 完成类的初始化
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    // 此时运行时锁一定是锁定状态
    runtimeLock.assertLocked();
    
    // 拿到了cls类 (oc类或swift类)
    curClass = cls;
5.1 类的实现(完善继承链和isa链,双向绑定)
image.png

此处目的是cls的所有内容赋值,完成继承链isa指向链的完整绑定。便于后续沿着继承链搜索sel对应的imp

5.2 类的初始化

检查本类继承链上的类是否都初始化,确保后续可以对类进行操作。

image.png

6.当前类和父类循环查找imp

   // unreasonableClassCountt为 353056。一个足够大的值,只要比继承链类的个数大,就循环有效)
    for (unsigned attempts = unreasonableClassCount();;) {
        // 在curClass的函数列表中搜索sel方法
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        // 如果存在,将imp赋值为sel对应的imp。跳到下面done继续操作
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        
        // 如果没有找到, curClass赋值为curClass的父类,判断父类是否存在
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // imp取初始值forward_imp(错误提示),跳出for操作。
            imp = forward_imp;
            break;
        }

        // 防止无限循环
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // 【进入循环】
        // cache_getImp走汇编,循环从父类缓存中高速寻找sel对应的Imp
        imp = cache_getImp(curClass, sel);
        
        // 如果imp是初始值forward_imp(错误提示),就跳出循环
        if (slowpath(imp == forward_imp)) {
            break;
        }
        
        // 在父类中找到imp后,跳到下面done继续操作
        if (fastpath(imp)) {
            goto done;
        }
    }
image.png

7.最后的防线

我们看到,上一步操作中,如果没找到imp,都会break跳出for操作,进入下面的动态尝试

  • 这是慢速查找最后的倔强,也是系统给予sel方法的最后一次补救机会。
// behavior 默认为3, LOOKUP_RESOLVER为2
 if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER; // behavior取LOOKUP_RESOLVER的反
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

这段代码只执行一次

    1. 首次进入:behavior=3, LOOKUP_RESOLVER = 2, behavior & LOOKUP_RESOLVER = 2 (二进制计算), if条件成立。
    1. behaviorLOOKUP_RESOLVER,下一次behaviorLOOKUP_RESOLVER进行与运算时,结果一定为0
      (比如 001 取反是 110, 他们每个位相反,所以与运算的结果为000
  • 所以这个补救机会,只会进入一次。

  • 这一步包括动态方法决议三步挽救法,我们在下一节详细讲解。

本节我们只需要知道,如果最后挽救失败,程序就会抛出forward_imp异常内容。并crash了。

8.查找结果

  • 当我们找到imp时,就会直接进入done流程,将clsselimp写入缓存中(便于下次同样的方法,可以在cache汇编快速查询到)。

  • done结束后,我们会进入done_nolock流程, 如果impforward_imp,就返回nil,否则,返回正常的imp

done:
    // 从将sel和imp写入cls的缓存
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    // 运行时解锁
    runtimeLock.unlock();

 done_nolock:
    //如果不需要找了(LOOKUP_NIL),并且imp等于forward_imp,就返回nil。
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    // 返回当前获取的imp
    return imp;

下一节,OC底层原理十四: objc_msgSend (消息转发 + 汇总图),我们会探究最后的防线,并梳理objc_msgSend完整流程

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