OC底层探索11-objc_msgSend慢速查找流程OC底层探索11-objc_msgSend慢速查找流程

2021-08-09 11:25:11 浏览数 (1)

在上一篇文章中介绍了在汇编部分的缓存快速查找流程。由于首次调用或者缓存扩容等问题导致的缓存查找失败,就需要进入慢速查找流程.

objc_msgSend慢速查找

慢速查找入口-汇编部分
  • 在快速查找流程无法找到对应缓存的时候,会跳到CheckMissJumpMiss这个macro中并且走到__objc_msgSend_uncached这个函数中。
代码语言:javascript复制
    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup   //跳转
    TailCallFunctionPointer x17 //寄存器调用

    END_ENTRY __objc_msgSend_uncached
  • 继续探索MethodTableLookup的实现,发现实现很长,但是都是一些寄存器的操作等,只看我们关心的b、bl
代码语言:javascript复制
.macro MethodTableLookup
...

// 省略影响跟流程的代码
bl  _lookUpImpOrForward

...
  • 继续搜索_lookUpImpOrForward发现找不到对用的实现了。苹果编码有一个习惯通过前缀增加、减少_,来实现多种语言的切换。

通过调用lookUpImpOrForward,就来到了c 部分。

通过调试来跟踪流程

前文中的流程跳转有一部分猜测的成分,现在通过调试来验证一下之前的猜测.

  1. 打开断点,找到目标调用
  1. 打开堆栈信息选项
  1. 按住control 点击 Step info
  1. 点击下一步来到__objc_msgSend_uncached
  1. 最终调用lookUpImpOrForward,而且给出了c 函数的位置

通过编译调试进一步验证了之前的猜测。

慢速查找c 流程
代码语言:javascript复制
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //参数准备
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    
    //再进行一次缓存的查找
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    
    runtimeLock.lock();    //加锁
    
    // TODO: this check is quite costly during process startup.
    //判断该类是否是已知类
    //这个操作需要从已知类表中做耗时的查找动作;推测以后苹果会优化点
    checkIsKnownClass(cls);
    
    //判断当前类是否加载到内存中(isa和rw信息是否存在)。
    if (slowpath(!cls->isRealized())) {
        //如果不存在进行类的加载,并且会递归加载所有父类、元类。为了确定类的父类、isa链
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }
    
    //判断类是否初始化,同样是递归判断所有
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        //对类进行初始化,并且会递归加载所有父类、元类。为了确定类的父类、isa链
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }
    
    runtimeLock.assertLocked();    //解锁
    
    //循环查找,无跳出条件
    for (unsigned attempts = unreasonableClassCount();;) {
        //使用二分法在当前类的方法列表methodList中查找该方法
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
        //本质就是查找方法的imp实现
            imp = meth->imp;
            goto done;
        }

        //1. 将当前类切换为父类
        //2. 如果当前类的父类是nil,也就是在NSObject中都没有找到则直接跳出循环
        if (slowpath((curClass = curClass->superclass) == nil)) {
            imp = forward_imp;
            break;
        }

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

        //对父类进行方法查找
        //1.汇编缓存快速查找
        imp = cache_getImp(curClass, sel);
        //如果父类没找到,首次会进入动态方法解析
        if (slowpath(imp == forward_imp)) {
            break;
        }
        //在父类中找到了该方法
        if (fastpath(imp)) {
            goto done;
        }
    }
    
    //每个类首次都会进行一次方法动态解析
    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;
    }
    //返回查询结果imp
    return imp;

这就是整个消息imp查询闭环流程(快、慢)。

慢速查找流程图

objc_msgSend慢速流程.png

cache_getImp没有发生递归
代码语言:javascript复制
STATIC_ENTRY _cache_getImp

GetClassFromIsa_p16 p0
CacheLookup GETIMP, _cache_getImp

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
  1. 可以看到CacheLookup的第一个参数是GETIMP
  2. GETIMP下并没有调用__objc_msgSend_uncached,所以并不会产生递归,只会查找当前类的缓存
方法没有找到,而且没有实现方法解析

extern void _objc_msgForward_impcache(void);看到extern就知道它又是汇编代码编写的了

代码语言:javascript复制
STATIC_ENTRY __objc_msgForward_impcache
//跳转到 __objc_msgForward
b   __objc_msgForward
END_ENTRY __objc_msgForward_impcache


ENTRY __objc_msgForward
//定位
adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
//寄存器操作
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
  • 但是全局搜索__objc_forward_handler没有找到下文了,猜测它是c 编写,所以取一个_继续搜索
  • 相信这段话已经很熟悉了吧?平时开发应该见过无数次了。
MethodList(有序数组)二分查找

以上已经解释了慢速查找的整个流程,现在对MethodList二分查找的实现做一个解释。(核心方法findMethodInSortedMethodList)

代码语言:javascript复制
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    ASSERT(list);
    //当前list是有序的,在类创建的时候就已经进行了加载
    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key; //key 等于 say666
    uint32_t count;
    //base相当于low,count是max,probe是middle,这就是二分
    for (count = list->count; count != 0; count >>= 1) {
        //从首地址 下标 --> 移动到中间位置(count >> 1 右移1位即 count/2 = 4)
        probe = base   (count >> 1); 
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        //如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置
        if (keyValue == probeValue) { 
            // -- while 平移 -- 找出分类重名方法
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                //找出分类重名方法
                //如果是两个分类,就看谁先进行加载
                probe--;
            }
            return (method_t *)probe;
        }
        
        //如果keyValue 大于 probeValue,就往probe即中间位置的右边查找
        if (keyValue > probeValue) { 
            base = probe   1;
            count--;
        }
    }
    return nil;
}

示例图:

1. 向左查询

8中找1

2. 向右查询

8中找7

0 人点赞