IOS底層-objc_msgSend慢速方法查找

快速查找流程中哼勇,如果沒有找到方法實(shí)現(xiàn)都伪,無論是走到CheckMissJumpMiss,最終都會(huì)走到__objc_msgSend_uncached匯編函數(shù)

  • 在主流環(huán)境arm64環(huán)境為條件條件积担,在objc-msg-arm64.s文件中查找__objc_msgSend_uncached的匯編實(shí)現(xiàn),核心是MethodTableLookup猬仁,源碼如下
STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

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

    END_ENTRY __objc_msgSend_uncached
  • 搜索MethodTableLookup帝璧,核心是lookUpImpOrForward,源碼如下:
.macro MethodTableLookup
    
    stmfd   sp!, {r0-r3,r7,lr}
    add r7, sp, #16
    sub sp, #8          // align stack
    FP_SAVE

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
.if $0 == NORMAL
    // receiver already in r0
    // selector already in r1
.else
    mov     r0, r1          // receiver
    mov     r1, r2          // selector
.endif
    mov r2, r9          // class to search
    mov r3, #3          // LOOKUP_INITIALIZE | LOOKUP_INITIALIZE
    blx _lookUpImpOrForward
    mov r12, r0         // r12 = IMP
    
.if $0 == NORMAL
    cmp r12, r12        // set eq for nonstret forwarding
.else
    tst r12, r12        // set ne for stret forwarding
.endif

    FP_RESTORE
    add sp, #8          // align stack
    ldmfd   sp!, {r0-r3,r7,lr}

.endmacro

通過匯編進(jìn)行分析

  • main函數(shù)[person say666];中打斷點(diǎn)湿刽,通過Debug->Debug Worldflow->Always Show dissasembly進(jìn)入?yún)R編的烁,如圖

    image

  • _objc_msgSend_uncached加一個(gè)斷,controller+step into進(jìn)入诈闺,如圖

    image

最后走到的就是lookUpImpOrForward渴庆,此時(shí)并不是匯編實(shí)現(xiàn)

C/C++中調(diào)用 匯編 ,去查找匯編時(shí)雅镊,C/C++調(diào)用的方法需要多加一個(gè)下劃線
匯編中調(diào)用C/C++方法時(shí)襟雷,去查找C/C++方法,需要將匯編調(diào)用的方法去掉一個(gè)下劃線

慢速查找 - C/C++

  • 匯編部分的提示仁烹,全局搜索lookUpImpOrForward耸弄,在objc-runtime-new.mm文件中找到了源碼實(shí)現(xiàn),這是一個(gè)c實(shí)現(xiàn)的函數(shù)
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
    //目的:防止多線程操作時(shí)计呈,剛好調(diào)用函數(shù),此時(shí)緩存進(jìn)來了
    if (fastpath(behavior & LOOKUP_CACHE)) { 
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    
    //加鎖征唬,目的是保證讀取的線程安全
    runtimeLock.lock();
    
    //判斷是否是一個(gè)已知的類:判斷當(dāng)前類是否是已經(jīng)被認(rèn)可的類捌显,即已經(jīng)加載的類
    checkIsKnownClass(cls); 
    
    //判斷類是否實(shí)現(xiàn),如果沒有总寒,需要先實(shí)現(xiàn)扶歪,此時(shí)的目的是為了確定父類鏈,方法后續(xù)的循環(huán)
    if (slowpath(!cls->isRealized())) { 
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    //判斷類是否初始化偿乖,如果沒有击罪,需要先初始化
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) { 
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;

    //----查找類的緩存
    
    // unreasonableClassCount -- 表示類的迭代的上限
    //(猜測(cè)這里遞歸的原因是attempts在第一次循環(huán)時(shí)作了減一操作,然后再次循環(huán)時(shí),仍在上限的范圍內(nèi)贪薪,所以可以繼續(xù)遞歸)
    for (unsigned attempts = unreasonableClassCount();;) { 
        //---當(dāng)前類方法列表(采用二分查找算法)媳禁,如果找到,則返回画切,將方法緩存到cache中
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //當(dāng)前類 = 當(dāng)前類的父類竣稽,并判斷父類是否為nil
        if (slowpath((curClass = curClass->superclass) == nil)) {
            //--未找到方法實(shí)現(xiàn),方法解析器也不行,使用轉(zhuǎn)發(fā)
            imp = forward_imp;
            break;
        }

        // 如果父類鏈中存在循環(huán)毫别,則停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // --父類緩存
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) { 
            // 如果在父類中找到了forward娃弓,則停止查找,且不緩存岛宦,首先調(diào)用此類的方法解析器
            break;
        }
        if (fastpath(imp)) {
            //如果在父類中台丛,找到了此方法,將其存儲(chǔ)到cache中
            goto done;
        }
    }

    //沒有找到方法實(shí)現(xiàn)砾肺,嘗試一次方法解析

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //動(dòng)態(tài)方法決議的控制條件挽霉,表示流程只走一次
        behavior ^= LOOKUP_RESOLVER; 
        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;
}

步驟如下:

  • 【第一步】cahce緩存中進(jìn)行查找即快速查找,找到并直接返回imp变汪,反之則進(jìn)【第二部】

  • 【第二步】判斷cls

    • 是否已知類,有才能找到緩存
    • 類是否實(shí)現(xiàn)侠坎,如果沒有,則需要先實(shí)現(xiàn)裙盾,確定其父類鏈实胸,此時(shí)實(shí)例化的目的是為了確定父類鏈、ro番官、以及rw等庐完,方法后續(xù)數(shù)據(jù)的讀取以及查找的循環(huán)
    • 是否初始化,如果沒有鲤拿,則callInitialize初始化
  • 【第三步】for循環(huán)假褪,類繼承鏈 或者 元類繼承鏈的順序查找

    • 當(dāng)前cls的方法列表中使用二分查找算法查找方法,如果找到近顷,則進(jìn)入cache寫入流程并返回imp生音,如果沒有找到,則返回nil窒升,繼續(xù)循環(huán)缀遍。
    • 查找父類,如果沒有找饱须,imp置換成forward_imp域醇,進(jìn)行消息轉(zhuǎn)發(fā),并終止遞歸進(jìn)入【第四步】
    • 父類鏈中存在循環(huán)蓉媳,則報(bào)錯(cuò)譬挚,終止循環(huán)
  • 【第四步】判斷是否執(zhí)行過動(dòng)態(tài)方法解析

    • 如果沒有,執(zhí)行動(dòng)態(tài)方法解析
    • 如果執(zhí)行過一次動(dòng)態(tài)方法解析酪呻,則走到消息轉(zhuǎn)發(fā)流程

getMethodNoSuper_nolock方法:二分查找方法列表

二分查找的核心代碼

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    ASSERT(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key; //key 等于 say666
    uint32_t count;
    //base相當(dāng)于low减宣,count是max,probe是middle玩荠,這就是二分
    for (count = list->count; count != 0; count >>= 1) {
        //從首地址+下標(biāo) --> 移動(dòng)到中間位置(count >> 1 左移1位即 count/2 = 4)
        probe = base + (count >> 1); 
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        //如果查找的key的keyvalue等于中間位置(probe)的probeValue漆腌,則直接返回中間位置
        if (keyValue == probeValue) { 
            // -- while 平移 -- 排除分類重名方法
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                //排除分類重名方法(方法的存儲(chǔ)是先存儲(chǔ)類方法贼邓,在存儲(chǔ)分類---按照先進(jìn)后出的原則,分類方法最先出闷尿,而我們要取的類方法盟猖,所以需要先排除分類方法)
                //如果是兩個(gè)分類福侈,就看誰先進(jìn)行加載
                probe--;
            }
            return (method_t *)probe;
        }
        
        //如果keyValue 大于 probeValue肖油,就往probe即中間位置的右邊查找
        if (keyValue > probeValue) { 
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

算法原理簡(jiǎn)述為:從第一次查找開始艰额,每次都取中間位置,與想查找的key的value值作比較劳景,如果相等绑咱,則需要排除分類方法,然后將查詢到的位置的方法實(shí)現(xiàn)返回枢泰,如果不相等,則需要繼續(xù)二分查找铝噩,如果循環(huán)至count = 0還是沒有找到衡蚂,則直接返回nil,如下所示:


二分查找方法列表

動(dòng)態(tài)方法分析

動(dòng)態(tài)方法的源碼如下骏庸,在沒有找到實(shí)現(xiàn)的放方法時(shí)會(huì)崩潰毛甲,因此蘋果爸爸resolveInstanceMethod通過這個(gè)方法再給一次機(jī)會(huì)進(jìn)行特殊處理。我們將在下一篇進(jìn)行詳細(xì)討論具被。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了",NSStringFromSelector(sel));
        
        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末玻募,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子一姿,更是在濱河造成了極大的恐慌七咧,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叮叹,死亡現(xiàn)場(chǎng)離奇詭異艾栋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蛉顽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蝗砾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人携冤,你說我怎么就攤上這事悼粮。” “怎么了曾棕?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵扣猫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我睁蕾,道長(zhǎng)苞笨,這世上最難降的妖魔是什么债朵? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮瀑凝,結(jié)果婚禮上序芦,老公的妹妹穿的比我還像新娘。我一直安慰自己粤咪,他們只是感情好谚中,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寥枝,像睡著了一般宪塔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上囊拜,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天某筐,我揣著相機(jī)與錄音,去河邊找鬼冠跷。 笑死南誊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蜜托。 我是一名探鬼主播抄囚,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼橄务!你這毒婦竟也來了幔托?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蜂挪,失蹤者是張志新(化名)和其女友劉穎重挑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锅劝,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攒驰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了故爵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玻粪。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖诬垂,靈堂內(nèi)的尸體忽然破棺而出劲室,到底是詐尸還是另有隱情,我是刑警寧澤结窘,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布很洋,位于F島的核電站,受9級(jí)特大地震影響隧枫,放射性物質(zhì)發(fā)生泄漏喉磁。R本人自食惡果不足惜谓苟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望协怒。 院中可真熱鬧涝焙,春花似錦、人聲如沸孕暇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妖滔。三九已至隧哮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間座舍,已是汗流浹背沮翔。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曲秉,地道東北人鉴竭。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像岸浑,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瑰步,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355