iOS-底层原理 07 消息的查询

269

方法的本质

在前面的文章:对象的本质中,使用Clang探索了对象的本质。同样我们使用Clang获取.cpp文件查看main中方法的实现。

//main.m中的代码
HTPerson *person = [HTPerson alloc];

[person sayNB];
[person playGame:@"王者荣耀"];
    
//编译成main.cpp后的代码
HTPerson *person = ((HTPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HTPerson"), sel_registerName("alloc"));
        
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("playGame:"), (NSString *)&__NSConstantStringImpl__var_folders_pb_ltmc_s613_76jpz77vf9cvx40000gn_T_main_dad2e6_mi_3);

可以看出,方法的其实就是一个objc_msgSend的消息,而方法的执行,其实就是查找方法的实现地址,并且执行该方法的过程。

//self是消息的接受者  SEL是方法名 在后面是一些参数
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

对象在收到objc_msgSend消息后,会根据isa指向的类中查找方法的实现地址。查找流程大致如下:

  • 快速查找:去过类的缓存cacte_t中有对应的方法实现,那么返回该方法地址。
  • 慢速查找:如果没有缓存,会去查找bits中的methods。如果没有继续去superclass查找相应的方法。

方法的快速查询(获取cache中的方法)

objc源码中搜索objc_msgSend会在objc-msg-arm64文件中看到ENTRY _objc_msgSend 。这里是使用汇编进行方法的查询。大致流程如下:

  • 判断objc_msgSend方法的第一个参数receiver是否为空,并且获取到isa
  • 进入到快速方法查询 CacheLookup
  • 通过首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址 占8字节,superClass占8字节),获取cahce
  • 从cache中分别取出bucketsmask,并由mask根据哈希算法计算出哈希下标
  • 递归查询对应的bucket中的 sel 和 message中的op是否一致,如果一致返回,不一致进入慢查询方法(__objc_msgSend_uncached

2251862-aecc50c741fa734c.png

方法的慢速查询

慢速查询的主要流程:

  • 从汇编中__objc_msgSend_uncached的方法到底层lookUpImpOrForward的实现
  • lookUpImpOrForward中主要做三件事
    • 初始化所有的父类
    • 死循环二分法查找类的方法,查找父类缓存的方法,再次二分法查询父类的方法...
    • 把获取到的方法imp缓存到cache中

初始化所有的父类

方法realizeAndInitializeIfNeeded_locked点进去里面有实现方法。判断类是否实现,如果没有,则需要先实现,确定其父类链,此时实例化的目的是为了确定父类链、ro、以及rw等,方法后续数据的读取以及查找的循环

二分法查找类的方法

方法getMethodNoSuper_nolock点进去。会发现

  • 根据向二进制位右移动一位的方法,实现二分法查询
  • 如果有相同的方法名称,会去取前面的方法名返回(分类的方法会存在同名本类方法的前面)

把获取到的方法imp缓存到cache中

方法log_and_fill_cache缓存方法的imp。 如果没有找到imp,则进行动态方法决议和消息转发。在下一节会介绍。

总结

  • 类方法执行的顺序是由查询顺序所决定的。(子类 - 父类 - NSObject)
  • 在二分法查询时,会决定先执行分类方法。
  • 找不到方法时会尝试动态方法决议
  • 如果动态方法决议仍然没有找到,则进行消息转发 源码如下所示
//   缓存 找不到 - lookUpImpOrForward - 慢速 - methodlist
//   汇编 缓存 参数未知

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    
    //    创建forward_imp,并给定默认值_objc_msgForward_impcache
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    
    //    创建imp,用于接收通过sel查找的imp
    IMP imp = nil;
    
    //    创建要查找的类,这个类通过isa的指向关系是会一直变化的,
    //    直到最终指向NSObject的父类nil为止
    Class curClass;
    
    runtimeLock.assertUnlocked();
    
    //    发送到类的第一条消息通常是 +new 或 +alloc 或 +self
    //    但是,此时该类尚未初始化,此时behavior = 3|8 = 11
    //    当向将上+new等这些方法inset进缓存的时候
    //    不满足behavior & LOOKUP_NOCACHE) == 0这个条件,8 & 11 = 8
    //    所以上述这些方法不会加载进缓存。
    //
    //    如果类已经初始化了,就不会修改behavior的值了,behavior=3
    //    我们自定义的方法是可以正常加载进缓存的。
    
    if (slowpath(!cls->isInitialized())) {
        
        behavior |= LOOKUP_NOCACHE;
    }
    
    //    runtimeLock 在 isRealized 和 isInitialized 检查期间被持有
    //    加锁-防止与并发实现竞争。
    runtimeLock.lock();
    
    //    检查是否是注册类
    checkIsKnownClass(cls);
    
    //    初始化跟cls实例对象在isa指向图中的每一个类(class和metaClass)
    //    以便后续自己类里面找不到方法去父类里面找
    //    依次向上找
    //    所以在此处对所有相关的类进行了初始化
    
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    
    //   curClass为当前实例对象的类
    curClass = cls;
    
    //    死循环查找
    //    循环查找类对象的methodList,当前类没有的话就找父类
    //    父类没有就找父类的父类,一直找到NSObject类
    //    如果NSObject都找不到的话最终curClass会指向nil
    //    将事先准备好的forward_imp赋值给imp
    //    然后结束慢速查找流程,接下来进入Runtime消息转发机制
    
    for (unsigned attempts = unreasonableClassCount();;) {
        
        //    第一步先找共享缓存里面有没有我们的方法
        //    通常情况下我们的自定义方法不会出现在共享缓存中
        
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            
            //     在当前类的方法列表里面查找,这是重点
            //     查找算法是二分法
            
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            //如果找到了方法
            if (meth) {
                imp = meth->imp(false);
                //跳转到done
                goto done;
            }
            
            //      如果当前类找不到,取将curClass指向superclass
            //      查询父类的methodList,一直找到NSObject的父类nil为止
            //      将事先准备好的forward_imp赋值给imp
            //      然后结束慢速查找流程,接下来进入Runtime消息转发机制
            //      结束循环遍历
            
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                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)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        //如果找到了
        if (fastpath(imp)) {
            //缓存
            // Found the method in a superclass. Cache it in this class.
            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://缓存
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        //imp加入到缓存
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
done_unlock:
    //解锁
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

慢查询流程图如下

2251862-fd926e375c27a218-2.png

二分法查询实现如下

2251862-3e3b988e3e02b757.png

参考:
月月的方法快速查询慢速查询
bblv的快速查询