9.iOS底层学习之方法的慢速查找流程

上一篇学习了objc_msgSend的过程,先来回顾下流程:
-先通过enter进入到objc_msgSend函数,然后去判断消息的接受者是不是为空;
-如果为空直接返回0;
-如果不为空通过对象找到isa近而找到cache,然后通过内存平移找到buckets;
-找到buckets开始从指定的index,从后往前进行遍历,直到找到对的sel把相应的imp返回给消息接受者;
-如果找不到,会走MissLabelDynamic,接下来要讨论的流程,也就是慢速查找流程

我们通过代码CacheLookup过程发现MissLabelDynamic,传进来的参数对应的是__objc_msgSend_uncached

image.png

//CacheLookup 的宏定义
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

所以,我们具体来看下__objc_msgSend_uncached的实现。

__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

这段代码很简短,可以看到调用了两个关键的方法MethodTableLookup,和TailCallFunctionPointer。分别查看下方法的具体实现。

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
    RESTORE_REGS MSGSEND
.endmacro
TailCallFunctionPointer
.macro TailCallFunctionPointer
    // $0 = function pointer value
    br  $0
.endmacro

结合这两段代码的具体实现可以了解到MethodTableLookup 通过_lookUpImpOrForward方法找到相应的实现后赋值给x17,TailCallFunctionPointer直接返回x17并跳转到相应的地址。

\color{red}{汇编指令:}bl, 跳转到某地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转 ;一般用于不同方法直接的调用 ,例如:

// 先将下一指令地址(‘0x100cfa754’ 函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘0x100cfa754’ 函数
bl 0x100cfa754  ; 

\color{red}{汇编指令:}br, 跳转到某寄存器(的值)指向的地址(无返回), 不会改变 lr (x30) 寄存器的值。

经过上述整体的了解,我们接下来来看_lookUpImpOrForward具体做了什么,怎么查到的imp然后返回给x17。

lookUpImpOrForward

找到lookUpImpOrForward定义如下:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    runtimeLock.assertUnlocked();
    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    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 {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

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

以上流程梳理:
1、checkIsKnownClass先去检查是否注册类,未注册报错打印错误,会走_objc_fatalv方法;
2、通过方法realizeAndInitializeIfNeeded_locked下的initializeAndMaybeRelock去初始化类父类以及元类等isa的走位图和继承链,为慢速查找做好准备;
3、开始走for循环的遍历查询,先去isConstantOptimizedCache判断是不是要再去查缓存,如果需要进行再一次查缓存,是为了最后确认一次缓存里有没有相应的imp,如果有那么直接返回,走done_unlock;如果isConstantOptimizedCache为false那么会走getMethodNoSuper_nolock方法查询。
4、如果查到直接goto done;如果没有找到方法会curClass = curClass->getSuperclass()去查父类,先去查cache然后继续去走循环,直到跳转到done或者done_unlock,然后返回imp;

\color{red}{以上为慢速查找的整个流程。}然后来看下查找的具体实现:getMethodNoSuper_nolock。
我们通过getMethodNoSuper_nolock->search_method_list_inline->findMethodInSortedMethodList->findMethodInSortedMethodList找到查找方法的关键实现findMethodInSortedMethodList。

findMethodInSortedMethodList

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

以上是查找方法的算法,主要是运用二分查找,也叫折半查找。使用二分查找的条件是\color{red}{顺序存储结构},并且存储的数据是按照关键字大小有序排好的。二分查找的\color{red}{时间复杂度}是log以2为底n的对数。了解这些以后我们来详细了解一下方法列表的二分查找的源码。

以上源代码我们带入值进行实际的模拟运行一下:
假设 keyValue=7;
count = 10;
然后我们进入到算法findMethodInSortedMethodList;
\color{red}{第一趟查找}
first = 0 = base; count != 0;条件成立进入循环体,probe = base+(count >>1);
\color{red}{这里补充一下左移右移运算符的用法,左移右移相当于乘除法的运算,左移一位是当前的值乘以2的一次方,左移n位就是当前的值乘以2的n次方;同样右移n位表示当前的值除以2的n次方}
所以probe = base+(count >>1)就等同于 probe = base + count/2 = 0+10/2 = 5; 7==5条件为假,接着往下判断是7>5为真,base = probe+1 = 5+1 = 6;
count-- ,count = 9, count右移一位并赋值给自己变成9/2=4;
第一趟查找结束,继续循环;
\color{red}{第二趟查找}
probe = base + count/2 = 6+ 4/2 = 8;
7==8条件不成立;
7>8条件不成立,count右移一位变成4/2=2;第二趟结束;
\color{red}{第三趟查找}
probe = base + (count >> 1)=base + count/2=6+2/2 = 7;
7==7成立进入keyValue == probeValue条件体内,

 // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
           }
            return &*probe;

这个while和分类有关系。如果命中的位置前一个方法也和keyValue,优先去调用前一个,如果不等直接返回probe。一般当前方法的前一个方法也能命中就是分类也去实现了该方法,通过方法的列表的排列和写入,分类的方法是在类的前面的,后面学习分类的时候会进一步分析。

以上源代码整个折半查找大致的过程是这样:
1、拿到list的第一个位置的关键字,然后list的长度,还有待查询关键字;
2、list的count折半,先去查左半区,不相等判断下折半过后的关键字和待查询的关键字的大小,如果待查询的比折半后的大,说明待查询的在右半区,base会移到右半区从刚才查过了的中间的位置的后一个开始,然后count--很细节,我理解的是和while (probe > first && keyValue == (uintptr_t)getName((probe - 1)))这个地方的原因一样,优先去查靠前的方法也就是左侧区域的,有可能有分类再这个要查询的方法前边,这个地方就针对这个做了个优化,如果有分类的同名方法会少执行几次这个方法 while (probe > first && keyValue == (uintptr_t)getName((probe - 1)))(ps:欢迎补充和纠正,只是自己目前的理解,未必准确!
3、如果查找的关键字比base位置的关键字小,那么继续在左半区查,继续折半,以此类推,直到循环结束,结束的条件就是base和first重合了,此时count为1再右移为0,循环结束;
4、循环结束后,还是没查到返回nil;

以上为慢速查找的学习,接下来一篇将分析慢速查找也没查到该怎么办。

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

推荐阅读更多精彩内容