iOS消息传递objc_msgSend底层详解(下)

前言

上一篇讲解了objc_msgSend调用流程并在缓存中找到对应方法。今天我们来详解在缓存中找不到对应方法的情况。

回到objc_msgSend源码中调用cacheLookup方法的地方:

LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

cacheLookup定义:

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

当找不到缓存中方法的时候执行:

3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
    b.hs    1b

对应cacheLookup的调用及传参,可知当找不到缓存方法时会调用_objc_msgSend_uncached,这个方法其实之前在查看方法堆栈信息时已经见到过了:

insert之前的调用信息

接下来,来看看_objc_msgSend_uncached到底做了什么。

_objc_msgSend_uncached源码解析

本篇依然使用objc_msg_arm64.s来分析_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

方法很简洁,核心方法就在MethodTableLookupTailCallFunctionPointer x17,其中:

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

TailCallFunctionPointer其实就是返回x17,所以_objc_msgSend_uncached的真正核心就在于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

可看到值得关注的核心方法就是_lookUpImpOrForward,继续找他的源码,发现汇编里没有,终于回到了runtime-new.mm。方法代码很长,但包含了著名的慢速查找过程

慢速查找

lookUpImpOrForward源码中,我们需要关注的重点在于如何找到imp的,首先关注核心代码:

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

注意到这是一个没有终止条件和变量的死循环,跳出循环的地方就是我们要重点看的地方breakgoto。我们逐句分析所谓慢速查找到底是怎么做的:

if (curClass->cache.isConstantOptimizedCache(/* strict */true))

第一个if条件很有意思,是再去cache中找了一遍是否含有对应方法的imp,可以视为一种预防处理,比如有些方法插入出现了延时,在进入慢速查找之前还没插入完成,所以在正式进入慢速查找之前再检查确认一遍,避免出现不必要的慢速查找,提升效率。缓存中查找方法我们就不再赘述了,直接看else中的逻辑Method meth = getMethodNoSuper_nolock(curClass, sel);,其中getMethodNoSuper_nolock是:

static method_t *
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;
}
  • ASSERT(cls->isRealized());:判断cls是否已注册(以后会讲到);
  • auto const methods = cls->data()->methods();:这句大家很熟息了,取出当前class的方法列表(详见类的结构解析);
  • for循环遍历类的每一个方法列表(类的方法列表可能是二位数组class method_array_t : public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>),查找的核心方法search_method_list_inline去掉多余代码后为:
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;
    }
    return nil;
}

可以看到,查找的核心方法是findMethodInSortedMethodList(sel, mlist),继续查看他的源码找到核心方法指向了findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName),也就是慢速查找方法的重点二分查找法(为什么用二分法?因为一个类可能含有的方法可能数量很多):

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;
}
  • for循环的条件为(count = 方法数;count不等于0;count右移1位),这里右移1位相当于除以2,为了方便逻辑理解,这里假设count = 8(8-1000右移一位变为40100,相当于8除以2);
  • probe = base + (count >> 1);:base初始值为begin,也就是0,probe = 0 + (8>>1 = 4)= 4,for循环第一次查找的位置是4号位置;
  • keyValue就是传进来的SEL转成long类型的值,假设keyValue等于7,同样的probeValue就是probe转为long类型的值 uintptr_t probeValue = (uintptr_t)getName(probe),将两者相比较看取到的probe值是否等于传进来的SEL;
  • 如果相等,意为找到了对应方法。这之间还存在一个white循环,通过注释可知这是查找category中是否有重写当前方法,存在则执行probe--,这也证明了:当有category重写当前方法时,将执行category中的方法,并且,当有多个category重写同一方法时,会执行最后一个category中的方法。如果category中没有重写方法,则返回&*probe,也就是对应的imp;
  • 如果不相等,则判断如果SEL转成的值大于4,则base = probe + 1等于5,count--等于7,开始第二次循环
  • 第二次循环时,循环条件上count>>1也就是7>>1为3,probe = base + (count >> 1)即probe = 5 + (3>>1 = 1)= 6,即第二次查找在数组的第六位。再次没找到的话符合keyValue > probeValue,base = 6+1 = 7,count-- = 3 - 1 = 2,开始第三次循环;
  • 第三次循环,循环条件上count>>1也就是2>>1为1,probe = base + (count >> 1)即probe = 7 + (1>>1 = 0)= 7,即第三次查找在数组的第七位,找到了对应方法。
  • 如果keyValue小于4会怎么样大家可以自己走一遍,体会一下count在循环中连续右移1位的精妙之处。
  • 如果一直没有找到,则最终返回nil。
    至此,慢速查找过程的二分查找法分析完毕。

像父类中查找方法

重新回到lookUpImpOrForward中,如果在方法列表中没有找到对应方法,那么接下来:

if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
     // No implementation found, and method resolver didn't help.
     // Use forwarding.
     imp = forward_imp;
     break;
}

这里其实做了两步操作,首先在if条件中,将curClass赋值为了当前类的父类curClass = curClass->getSuperclass(),如果有父类,则跳过;如果当前类没有父类,意味着当前类已经是根类,要查找的方法在本类至根类都没有对应方法的实现,因此返回了forward_imp开始进入消息转发流程,这一点其实在之前类的方法归属中探究过,当查找一个类中是否有对应方法的实现时,即使没有实现该方法,也会有imp返回值,返回的是_objc_msgForward,消息转发将在下一篇重点讲解。

再继续,获取到父类之后:

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (fastpath(imp)) {
     // Found the method in a superclass. Cache it in this class.
    goto done;
}

其中cache_getImp需要我们回到汇编中:

STATIC_ENTRY _cache_getImp

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

CacheLookup代码我们已经很熟悉了,跟之前objc_msgSend中的流程一样,获取isa->class,然后在缓存中查找方法,如果父类缓存中有的对应方法imp的话同样执行goto done;。但是这里要注意区别:在第一次FCPerson调用CacheLookup过程中第一个参数是NORMAL,NORMAL模式使得缓存中找不到imp后执行objc_msgSend_uncached,但是现在查到父类的时候,传参为GETIMP,我们来看区别:

.if \Mode == GETIMP
    b.ne    \MissLabelConstant      // cache miss
    sub x0, x16, x17, LSR #32       // imp = isa - imp_offs
    SignAsImp x0
    ret
.else
    b.ne    5f              // cache miss
    sub x17, x16, x17, LSR #32      // imp = isa - imp_offs
.if \Mode == NORMAL
    br  x17
.elseif \Mode == LOOKUP
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    SignAsImp x17
    ret
.else

可以看到当Mode = GETIMP时,查找不到会直接返回空,因此慢速查找过程中,父类缓存中没找到,不会调用uncache方法,而是直接返回nil然后继续lookUpImpOrForward中的循环,而再次循环时cls已经是superClass了,重新对父类再次执行慢速查找的流程,直至找到根类,其实整个查找的过程就递归的过程。

接下来我们继续分析找到方法的imp后,goto 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(cls, imp, sel, inst, curClass);就是说当一个本不在缓存中的方法找到imp后,将会进行缓存填充,把新的方法放入缓存中,方便下一次可以直接在缓存中找方法,不需要再次经过慢速查找:

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

看到cls->cache.insert(sel, imp, receiver);,终于达到了终点insert方法!

总结

综合之前几篇我们的学习,可以得到一个整体的流程:
1.类的结构中有方法列表methods和缓存的方法列表cache_t;
2.对对象发送消息时执行objc_msgSend
3.objc_msgSend时会先在cache中找,没有就会调用objc_msgSend_uncached走到lookUpImpOrForward方法进行循环慢速查找,通过二分法去methods中查找;
4.如果没找到则会开始递归,到父类的缓存中找,找不到的话不会调用objc_msgSend_uncached,而是返回空并且继续lookUpImpOrForward循环,这次循环时,查找的目标cls变为superClass,对superClass进行慢速查找,最终到根类;
4.如果在cache中找到,则直接执行;如果在methods中找到,则把方法插入缓存insert;
5.如果都没有,开始消息转发流程。

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

推荐阅读更多精彩内容