iOS底层-方法慢速查找

995 阅读5分钟

前言

在前面的文章方法快速查找中,我们探究了Runtime快速查找缓存的方法,当缓存没有找到时会进行慢速查找。本文将对方法的慢速查找过程进行探究。

__objc_msgSend_uncached

  • 方法快速查找找不到时,会走MissLabelDynamic方法,而这个方法对应传入的是__objc_msgSend_uncached,我们来全局搜下:
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
  • 里面有两个方法,先来看下TailCallFunctionPointer

TailCallFunctionPointer

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

这里只有一个跳转操作

  • 再看看MethodTableLookup

MethodTableLookup

.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   // 将x0赋值,给x17

    RESTORE_REGS MSGSEND

.endmacro
  • 先看mov x17, x0x0是第一个寄存器,是接收返回值的,所以imp肯定是在上面返回的,然后我们定位到_lookUpImpOrForward,搜索发现只在这里,然后去掉_后再搜索就找到C++中的代码

截屏2021-07-04 00.09.29.png

这里有个疑问:为什么查找缓存用汇编写呢?
1. 汇编更接近机器语言,运行的速度比较快,更加动态化
2. 比较安全
3. 查找方法时,很多参数是动态的不确定的,C或C++参数都是确定的,而汇编的参数是动态化的,刚好满足。

lookUpImpOrForward(慢速查找)

  • 因为我们要寻找的目标是IMP的返回,所以我们直接找IMP返回即可,在for (unsigned attempts = unreasonableClassCount();;)这个循环里,有对IMP的赋值,在分析主要流程之前,先来看看前面做了什么

查找准备

    1. 先判断类有没有加载:if (slowpath(!cls->isInitialized())),如果没加载behavior |= LOOKUP_NOCACHE
    1. 如果类已经加载,判断是不是注册类checkIsKnownClass(cls)
    1. cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE),这个方法是对类、父类、元类rorw进行赋值,是为了后面的for循环查找做好准备。

循环查找

  • 循环中的共享缓存不看,然后看getMethodNoSuper_nolock

getMethodNoSuper_nolock

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

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
  • 这里根据类拿到方法列表,然后从beginListsendLists开始遍历,每查找一次,都会调用search_method_list_inline方法
二分查找
  • search_method_list_inline里查看排序过的方法,最终找到了findMethodInSortedMethodList,这是一个著名的二分查找算法:
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

二分查找流程图如下

二分查找流程图

截屏2021-07-04 09.05.15.png

二分查找用例

我们举例来验证下二分查找流程:

    1. 数组count8keyValue为6base = 0
list: 1 2 3 4 5 6 7 8
期望 6
count = 8
base = 0
first = 0

查找过程如下流程:

截屏2021-07-04 09.31.25.png 这个情况只需要两次遍历就得到了结果,当计算的结果小于预期时,就会改变base值,继续查找

    1. 数组count8keyValue为2base = 0
list: 1 2 3 4 5 6 7 8
期望 2
count = 8
base = 0
first = 0

再来看看这个情况:

截屏2021-07-04 09.45.10.png 这里也是两次找到结果,当计算的结果大于预期时,base值不变,继续查找

  • 通过这两个例子,能感觉到二分查找的魅力与精妙,感觉设计这个算法的人简直太厉害了。

cache_getImp(没找到imp)

如果上面查找没有找到imp,会往下走,有方法imp = cache_getImp(curClass, sel)

// Superclass cache.
imp = cache_getImp(curClass, sel);

注释上是父类缓存curClass怎么变成了父类呢,原来在判断的时候就赋值了if (slowpath((curClass = curClass->getSuperclass()) == nil)),也就是说子类没找到,会去找父类,直到找到NSObject为止。再来看看cache_getImp方法,搜索发现在汇编代码

STATIC_ENTRY _cache_getImp

GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
  • 又找到了CacheLookup流程,也就是快速查找,找完然后再走C++的慢速查找,但这个和之前不一样,:
STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0, 0
	CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

LGetImpMissDynamic:
	mov	p0, #0 // 返回nil
	ret

LGetImpMissConstant:
	mov	p0, p2
	ret

	END_ENTRY _cache_getImp

这里将#0p0,也就是imp = nil返回,然后再在父类中找。

log_and_fill_cache(找到imp)

如果循环遍历找到了imp,会走done方法:

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);
    }

抛开共享缓存不看,目标就在log_and_fill_cache

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);
}
  • 然后我们就看到了熟悉的方法insert,也就是我们进行cache分析的insert流程。
  • 整个过程就是准备查找条件->循环查找方法列表->进行二分查找没找到的话在父类再重新查找找到就insert到缓存

慢速查找流程图

整个慢速查找流程如下

截屏2021-07-05 21.15.22.png

总结

  • 慢速查找缓存大致分为3步:查找前准备(设置好ro、rw相关方法) -> 遍历方法列表,每一次都进行二分查找 -> 父类中查找
  • 比较难理解的应该是二分查找,当算法不太明白时,可以写一些实例,跟着步骤走就会豁然开朗。这个算法写的特别妙,大大降低了搜索时间。