iOS9 慢速查找流程

367 阅读7分钟

一,什么时候进入慢速查找

快速查找流程既缓存查找,如果缓存中没有查找到,下面就会进入方法慢速查找流程

二,__objc_msgSend_uncached

在objc源码全局搜索 STATIC_ENTRY __objc_msgSend_uncached

4B9DC820-753B-4867-920D-ED9C84706B17.png

arm64源码内容

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p15 is the class to search
	
	MethodTableLookup
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached
  • 源码分析:核心部分也就 MethodTableLookupTailCallFunctionPointer x17

  • MethodTableLookup 调用c++代码执行慢速查找流程

  • TailCallFunctionPointer x17拿到上一步的查找结果跳转到x17寄存器

MethodTableLookup

  • 全局搜索 arm64源码
.macro MethodTableLookup
	
	SAVE_REGS MSGSEND

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro
  • 我们来看源码干了什么
  1. x16 = class 赋值给x2
  2. 3 赋值给 x3
  3. 跳转到 _lookUpImpOrForward去查找IMP
  4. 将查找到的IMP放进x0,并且将x0赋值给x17

TailCallFunctionPointer

//通过慢速查找流程找到了IMP,那么这里就是将跳转至寄存器x17 
//以提供后续汇编继续执行
.macro TailCallFunctionPointer
	// $0 = function pointer value
        // $0表示参数x17
	braaz	$0
.endmacro

.macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0
.endmacro

探究后学习到

  1. 快速查找流程没有找到会进入慢速查找流程
  2. 慢速查找流程__objc_msgSend_uncached会执行2条指令,MethodTableLookup去查找并返回impTailCallFunctionPointer跳转至x17寄存器,也就是imp所在的寄存器
  3. 慢速查找流程将会进入C++中执行,然后通过x0寄存器返回,继续在汇编中向下执行,方法查找流程最终会跳转至x17寄存器(无论是慢速查找还是快速查找

lookUpImpOrForward

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 imp = nil;//创建imp,用于接收通过sel查找的imp
    
    //创建要查找的类,这个类通过isa的指向关系是会一直变化的,
    //知道最终指向NSObject的父类nil为止
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
    /**发送到类的第一条消息通常是 +new 或 +alloc 或 +self 
    但是,此时该类尚未初始化,此时behavior = 3|8 = 11 
    当向将上+new等这些方法inset进缓存的时候 不满足behavior & LOOKUP_NOCACHE) == 0这个条件,
    8 & 11 = 8 所以上述这些方法不会加载进缓存。 
    如果类已经初始化了,就不会修改behavior的值了,
    behavior=3 我们自定义的方法是可以正常加载进缓存的。 */

        
        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;

class_getInstanceMethod().

/** * 循环查找类对象的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);
                goto done;
            }
            /** 
            *如果当前类找不到,取将curClass指向superclass *查询父类的methodList,一直找到NSObject的父类nil为止 
            * 将事先准备好的forward_imp赋值给imp 
            * 然后结束慢速查找流程,接下来进入Runtime消息转发机制 * 结束循环遍历 */

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }

// 类列表中的内存损坏
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

// 在父类的缓存中查找,这里再次进入汇编查找流程
        imp = cache_getImp(curClass, sel);
        //如果没有找到,将默认的forward_imp赋值给imp
        if (slowpath(imp == forward_imp)) {
            break;
        }
        //如果找到了
        if (fastpath(imp)) {
            //将找到的method插入到缓存中,以便下次查找使用快速缓存查找
            goto done;
        }
    }

//没有找到实现。尝试一次方法解析器。
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

//找到了sel对应的imp,将method方法加载进缓存
 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        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;
}

二分法查找流程(getMethodNoSuper_nolock)

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    
    //获取methodList,methodList因为数据类型的原因可能为二维数组 
    //循环条件是数组不为空,即开始位置不等于结束位置
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        //进入search_method_list_inline修复为有序list
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

修复函数(search_method_list_inline)

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name() == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

二分法查找(findMethodInSortedMethodList)

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    if (list->isSmallList()) {
        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
        } else {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
        }
    } else {
        return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
    }
}

二分法查找真正实现

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    //开始位置:0
    auto first = list->begin();
    //base开始也为0
    auto base = first;
    //probe也为0
    decltype(first) probe;
    //要查找imp对应的sel
    uintptr_t keyValue = (uintptr_t)key;
    //list的个数
    uint32_t count;
    
  
  /**
  二分法逻辑在很多地方都通用,能大大缩短查找次数
  就比如说 要从1-10000个编号其中有个号码是中奖的我们要去抽奖
  比较笨的方式是从1开始抽抽不到就抽2以此类推直到抽中
  二分法就是从10000/2 = 5000开始抽,如果抽中直接换奖抽不中会有人提示你你这个号大了还是小了 
  如果大了 下次抽奖号码就是 0+5000/2 = 2500
  如果小了 下次抽奖号码就是 5000+5000/2 = 7500
  每次抽奖都是在缩小中奖号码范围,这样的话,我们可以省去很多没必要的次数,快速抽中大奖,真是一件开心的事
 */
  
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
        //向前寻找第一个出现的imp,为了避免分类方法于主类方法相同时问题
        //这也就是为什么分类方法会被加载的原因
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
  • 二分法源码抽奖模仿
        auto first = 0;
        auto base = first;
        decltype(first) probe = 0;
        
        uintptr_t keyValue = 8888;
        uint32_t count;
        uint32_t current = 1;
        
        for (count = 10000; count != 0; count >>= 1) {
            
            probe = base + (count >> 1);
            NSLog(@"第%d次抽奖---抽奖号码是%d",current,probe);
            
            if (keyValue == probe) {
                NSLog(@"恭喜你中奖了,中奖号码是%d",probe);
                break;
            }
            else{
                NSLog(@"没中奖!!再接再厉");
            }
            
            if (keyValue > probe) {
                base = probe + 1;
                count--;
            }
            current += 1;
            
        }
  • 抽奖结果

截屏2021-08-21 下午7.58.17.png

慢速查找流程 lookUpImpOrForward找到imp之后 会调用log_and_fill_cache(缓存填充)

如果记录器允许,调用cache的insert方法,将其插入缓存,下一次的查找就会进行快速缓存查找了。

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}
  1. lookUpImpOrForward,此过程为慢速查找流程,
  2. 通过二分法的方式不断遍历来查找imp
  3. 对于全局性来讲,首先去找共享缓存
  4. 然后查自己的methodList,如果自己没有,找父类,父类没有,找NSObject
  5. NSObject没有,会指向nil,最终跳出来。
  6. 大致流程:共享缓存 -> methodList -> 父类 -> NSObject -> nil - >跳出循环