objc_msgSend流程分析之慢速查找

在快速查找流程中征懈,如果緩存命中了還好說(shuō),那么如果命不中呢,就會(huì)到我們的objc_msgSend慢速查找流程烘苹,這篇文章就好好來(lái)分析是怎么進(jìn)行慢速查找的

快速查找中如果沒(méi)有找到方法實(shí)現(xiàn)嘶是,發(fā)現(xiàn)無(wú)論是CheckMiss還是JumpMiss闻丑,最終都會(huì)走到__objc_msgSend_uncached匯編函數(shù)关顷,如圖

CheckMiss和JumpMiss

然后我們command+F 搜索一下__objc_msgSend_uncached這玩意干了啥

__objc_msgSend_uncached匯編實(shí)現(xiàn)

找到之后邢享,我們發(fā)現(xiàn)它做了一件事件洽腺,就是進(jìn)行了方法列表的查找脚粟。然后我們接著搜索MethodTableLookup,找到其源碼實(shí)現(xiàn)

MethodTableLookup匯編實(shí)現(xiàn)

前面的準(zhǔn)備工作匯編我們不需要管蘸朋,關(guān)鍵是bl _lookUpImpOrForward 這里跳轉(zhuǎn)到_lookUpImpOrForward核无,然后搜索_lookUpImpOrForward發(fā)現(xiàn)找不到了,那么去哪里了呢藕坯?

接下來(lái)我把項(xiàng)目跑起來(lái)团南,通過(guò)斷點(diǎn)來(lái)找到它

首先,在main函數(shù)中[person sayHello]處打一個(gè)斷點(diǎn)炼彪,然后我們打開(kāi)匯編調(diào)試吐根,在頂部狀態(tài)欄選擇Debug -- Debug worlflow -- 勾選Always show Disassembly, 運(yùn)行程序

匯編調(diào)試

運(yùn)行完了之后辐马,我們進(jìn)到objc_msgSend里面去拷橘,按住control,點(diǎn)中間調(diào)試按鈕(stepinto)就進(jìn)去了

objc_msgSend里面

來(lái)到objc_msgSend里面后我們發(fā)現(xiàn)下面有一個(gè)_objc_msgSend_uncached,看到這里喜爷,果斷打個(gè)斷點(diǎn)膜楷,然后接著往里面進(jìn)

_objc_msgSend_uncached里面

走進(jìn)來(lái)終于找到了我們的lookUpImpOrForwardobjc-runtime-new.mm里面第6099行 ,這就很爽了贞奋,我們直接找過(guò)去就行了。這里補(bǔ)充一點(diǎn)穷绵,細(xì)心的朋友可能會(huì)發(fā)現(xiàn)下劃線去哪里了轿塔, 這是因?yàn)閺奈覀兊?code>匯編到C++會(huì)少一個(gè)下劃線,從C++到C會(huì)再少一個(gè)下劃線,所以我們?cè)趨R編中去查找C/C++方法時(shí)要把下劃線給去掉

明白了這一點(diǎn)之后我們接下來(lái)就全局搜索lookUpImpOrForward勾缭,就來(lái)到了objc-runtime-new.mm里面的lookUpImpOrForward揍障, 這里面是通過(guò)C/C++寫(xiě)的,看起來(lái)還是要比匯編舒服一點(diǎn)

lookUpImpOrForward實(shí)現(xiàn)

還是老規(guī)矩俩由,我把這里面的代碼大致翻譯一下毒嫡,看我分析之前,大家也可以邊打斷點(diǎn)邊看我注釋?zhuān)@樣流程更加的清晰

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // 定義的消息轉(zhuǎn)發(fā)
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // 快速查找幻梯,如果找到則直接返回imp
    // 這個(gè)地方為什么又進(jìn)行了快速查找兜畸? 其目的是為了防止多線程操作時(shí),剛好調(diào)用函數(shù)碘梢,就可以有緩存了
    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        // 快速查找在緩存里面找到了imp咬摇,直接返回imp
        if (imp) goto done_nolock;
    }

    // 加鎖,目的是保證讀取的線程安全
    runtimeLock.lock();

    
    // TODO: this check is quite costly during process startup.
    
    // 判斷是否是一個(gè)已知的類(lèi):判斷當(dāng)前類(lèi)是否是已經(jīng)被認(rèn)可的類(lèi)煞躬,即已經(jīng)加載的類(lèi)
    checkIsKnownClass(cls);

    // 判斷類(lèi)是否實(shí)現(xiàn)肛鹏,如果沒(méi)有,需要先實(shí)現(xiàn)恩沛,此時(shí)的目的是為了確定父類(lèi)鏈在扰,方法后續(xù)的循環(huán)
    // 這個(gè)地方?jīng)]有imp,不是重點(diǎn)
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    // 判斷類(lèi)是否初始化雷客,如果沒(méi)有芒珠,需要先初始化
    // 這個(gè)地方?jīng)]有imp,不是重點(diǎn)
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;
    
    // ------------重點(diǎn)在for循環(huán)里面----------------
    
    // unreasonableClassCount -- 表示類(lèi)的迭代的上限
    //(猜測(cè)這里遞歸的原因是attempts在第一次循環(huán)時(shí)作了減一操作佛纫,然后再次循環(huán)時(shí),仍在上限的范圍內(nèi)妓局,所以可以繼續(xù)遞歸)
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        // 去當(dāng)前類(lèi)的方法列表查找(采用二分查找算法),如果找到呈宇,則goto done好爬,將方法緩存到cache中
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        
        //  -------- 如果二分查找找自己沒(méi)有找到,那么就開(kāi)始找父類(lèi)的緩存了 ----------
        // 注意甥啄, 這里將curClass = superclass 把父類(lèi)賦值給了當(dāng)前類(lèi)了存炮,并判斷父類(lèi)是否為nil
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            
            // 父類(lèi)全部找完了之后,父類(lèi)為nil了蜈漓, 把imp賦值為forward_imp
            imp = forward_imp;
            // 賦值完了之后會(huì)退出本次循環(huán)穆桂,說(shuō)明父類(lèi)也沒(méi)有這個(gè)方法
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        // 如果父類(lèi)鏈中存在循環(huán),則停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 找父類(lèi)的緩存融虽。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            
            // 如果在父類(lèi)中找到了forward享完,則退出循環(huán),調(diào)用此類(lèi)的方法解析器
            break;
        }
        
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            // 如果在父類(lèi)中有额,找到了此方法般又,將其存儲(chǔ)到cache中
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    // 在自己和父類(lèi)都沒(méi)有找到方法實(shí)現(xiàn)彼绷,則會(huì)來(lái)到動(dòng)態(tài)方法決議
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // 這里是此次動(dòng)態(tài)方法決議的控制條件
        behavior ^= LOOKUP_RESOLVER;
        // 動(dòng)態(tài)方法決議,給一次處理的機(jī)會(huì)
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    // 存儲(chǔ)到緩存
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    // 解鎖
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

這個(gè)代碼流程分析的話我覺(jué)得要比上一篇文章的匯編要容易看懂一些茴迁。不過(guò)這里還是把整個(gè)慢速查找的流程總結(jié)一下

總結(jié):

1寄悯、 首先cache緩存中快速查找沒(méi)有找到,則進(jìn)入慢速查找lookUpImpOrForward里面堕义。來(lái)到這里之后還會(huì)進(jìn)行快速查找一次猜旬,防止多線程操作時(shí),剛好調(diào)用了此函數(shù)倦卖,這樣就可以直接從緩存中快速查找洒擦, 不用再進(jìn)行慢速查找了

2、判斷cls糖耸, 是否是已知類(lèi)秘遏。 然后該類(lèi)是否實(shí)現(xiàn),如果沒(méi)有嘉竟,則需要先實(shí)現(xiàn)邦危,此時(shí)實(shí)例化的目的是為了確定父類(lèi)鏈、ro舍扰、以及rw等倦蚪,為后面的數(shù)據(jù)讀取查找做好準(zhǔn)備。

3边苹、 來(lái)到for循環(huán)陵且,慢速查找的重點(diǎn)流程 :

3.1: 去當(dāng)前類(lèi)的方法列表查找(采用二分查找算法),如果找到个束,則goto done慕购,將方法緩存到cache中,如果沒(méi)找到則開(kāi)始找父類(lèi)

3.2: 如果父類(lèi)找到了imp茬底,則直接返回imp沪悲,執(zhí)行cache寫(xiě)入流程,如果循環(huán)完所有的父類(lèi)還沒(méi)找到阱表,最終會(huì)找到nil殿如,父類(lèi)為nil的話就會(huì)走到imp = forward_imp,跳出當(dāng)前循環(huán)

4最爬、 當(dāng)前類(lèi)和父類(lèi)都沒(méi)找到imp涉馁, 就會(huì)來(lái)到動(dòng)態(tài)方法決議,給一次機(jī)會(huì)重新進(jìn)行查詢爱致,如果進(jìn)行處理了烤送,能拿到imp,就不會(huì)崩潰糠悯。

看完我這個(gè)總結(jié)帮坚,再去看我源碼里的注釋?zhuān)矣X(jué)得應(yīng)該非常清晰了牢裳,這就是整個(gè)方法的慢速查找流程。

在分析完objc_msgSend慢速查找之后叶沛,我再補(bǔ)充兩個(gè)知識(shí)點(diǎn)

二分法算法

首先第一個(gè)知識(shí)點(diǎn)是查找方法列表的時(shí)候用的二分法查找,我們先來(lái)找到二分法算法的源碼

二分法算法源碼

這個(gè)代碼我覺(jué)得非常的簡(jiǎn)單忘朝, 沒(méi)有必要再一個(gè)個(gè)注釋了灰署,如果有看不懂的可以斷點(diǎn)一試就明白了,也可以給我留言局嘁,我收到后會(huì)及時(shí)回復(fù)大家溉箕,我說(shuō)一下大致的算法流程:

1、 拿到方法列表的第一個(gè)first和總數(shù)悦昵,然后開(kāi)始遍歷循環(huán)肴茄。注意,方法列表里面的數(shù)值是遞增的但指,有序的

2寡痰、進(jìn)到循環(huán)之后,probe = base + (count >> 1); 這句代碼意思是首地址 位移 (count >> 1) 相當(dāng)于從第一個(gè)元素移動(dòng)到了中間

3棋凳、 移動(dòng)到了中間之后拦坠,拿keyValue == probeValue 進(jìn)行對(duì)比,如果等于剩岳,則返回method_t贞滨。注意,這里有一個(gè) probe-- 拍棕,意思是排除分類(lèi)里面名字一樣的方法晓铆。

4、如果keyValue 大于 probeValue绰播,即在(probe +1) 到 (count--)之間查找骄噪,同時(shí)count每循環(huán)一次,右移一位幅垮,雙數(shù)減半腰池,單數(shù)減半再減一,比如8右移一位是 8/2 = 4忙芒,7右移一位是 7/2 - 1 = 3示弓。然后再回到第二個(gè)步驟進(jìn)行查找

5、如果keyValue 小于 probeValue呵萨,即在1 - probe之間繼續(xù)取中間位置進(jìn)行查找奏属,同時(shí)count每循環(huán)一次,右移一位潮峦。然后再回到第二個(gè)步驟進(jìn)行查找

6囱皿、 一直循環(huán)到count0勇婴,probeValue1 還沒(méi)找到,就返回nil

消息轉(zhuǎn)發(fā)

明白了二分法算法之后嘱腥,我們最后再補(bǔ)充一個(gè)知識(shí)點(diǎn)耕渴,就是動(dòng)態(tài)方法決議之后,會(huì)來(lái)到消息轉(zhuǎn)發(fā)_objc_msgForward_impcache這里齿兔,這個(gè)是匯編實(shí)現(xiàn)的橱脸。

__objc_msgForward_impcache匯編

然后會(huì)走到__objc_msgForward,走到這里之后往下走分苇,會(huì)來(lái)到__objc_forward_handler添诉,我們搜索__objc_forward_handler找不到,因?yàn)閰R編加了下劃線医寿,我們?nèi)サ粢粋€(gè)下劃線試試栏赴,搜索_objc_forward_handler,就找到了C++文件里面靖秩,然后最終走到了objc_defaultForwardHandler里面须眷。這就是一直都沒(méi)有找到實(shí)現(xiàn)的方法,崩潰時(shí)報(bào)的錯(cuò)誤提示盆偿。

objc_defaultForwardHandler實(shí)現(xiàn)

最后柒爸,來(lái)一張消息轉(zhuǎn)發(fā)機(jī)制流程圖,為下一篇文章做準(zhǔn)備

消息轉(zhuǎn)發(fā)機(jī)制
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末事扭,一起剝皮案震驚了整個(gè)濱河市捎稚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌求橄,老刑警劉巖今野,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異罐农,居然都是意外死亡条霜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)涵亏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)宰睡,“玉大人,你說(shuō)我怎么就攤上這事气筋〔鹉冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵宠默,是天一觀的道長(zhǎng)麸恍。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么抹沪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任刻肄,我火速辦了婚禮,結(jié)果婚禮上融欧,老公的妹妹穿的比我還像新娘敏弃。我一直安慰自己,他們只是感情好噪馏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布权她。 她就那樣靜靜地躺著,像睡著了一般逝薪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蝴罪,一...
    開(kāi)封第一講書(shū)人閱讀 51,198評(píng)論 1 299
  • 那天董济,我揣著相機(jī)與錄音,去河邊找鬼要门。 笑死虏肾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的欢搜。 我是一名探鬼主播封豪,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼炒瘟!你這毒婦竟也來(lái)了吹埠?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤疮装,失蹤者是張志新(化名)和其女友劉穎缘琅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體廓推,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刷袍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了樊展。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呻纹。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖专缠,靈堂內(nèi)的尸體忽然破棺而出雷酪,到底是詐尸還是另有隱情,我是刑警寧澤藤肢,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布太闺,位于F島的核電站,受9級(jí)特大地震影響嘁圈,放射性物質(zhì)發(fā)生泄漏省骂。R本人自食惡果不足惜蟀淮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钞澳。 院中可真熱鬧怠惶,春花似錦、人聲如沸轧粟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)兰吟。三九已至通惫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間混蔼,已是汗流浹背履腋。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惭嚣,地道東北人遵湖。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晚吞,于是被迫代替她去往敵國(guó)和親延旧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354