iOS底層之objc_msgSend慢速查找流程

iOS底層之objc_msgSend快速查找流程里分析里調(diào)用方法的本質(zhì)乃坤,就是消息發(fā)送矢劲,查找類的方法緩存邀杏,那么如果經(jīng)歷CacheLookup后沒找到緩存驰徊,即快速查找流程找不到庶诡,則會開始慢速查找惦银,從methodList查找,這一篇文章我們來分析方法的查找流程末誓。

CacheLookup快速查找流程中扯俱,當(dāng)沒有找到方法imp緩存,無論是走到CheckMiss還是JumpMiss喇澡,最終都會走到__objc_msgSend_uncached匯編函數(shù)迅栅。其定義是:

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band r10 is the searched class

    // r10 is already the class to search
    MethodTableLookup NORMAL    // r11 = IMP
    jmp *%r11           // goto *imp

    END_ENTRY __objc_msgSend_uncached

可以看到關(guān)鍵代碼MethodTableLookup,會查找方法表晴玖。
其定義是:

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // 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 registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

其中關(guān)鍵代碼是_lookUpImpOrForward读存。為什么为流?
通過調(diào)用一個實(shí)例方法[person sayHello],并打斷點(diǎn)让簿。打開匯編調(diào)試查看:


可以看到匯編停在了[person sayHello]調(diào)用之前敬察,下一步開始發(fā)送消息調(diào)用sayHello。打斷點(diǎn)objc_msgSend拜英,按住control+點(diǎn)擊step into進(jìn)入內(nèi)部調(diào)用静汤。

_objc_msgSend_uncached之上完成了類信息獲取,方法快速查找流程居凶,沒找到緩存時來到了_objc_msgSend_uncached虫给,斷點(diǎn)跳到這一句,繼續(xù)step into進(jìn)入內(nèi)部侠碧。

可以看到來到了lookUpImpOrForward函數(shù)抹估,顯示這個函數(shù)在objc-runtime-new.mm文件的6099行。
也就驗(yàn)證了上面我們說的_objc_msgSend_uncached之后會來到_lookUpImpOrForward弄兜。

通過匯編源碼查找C/C++源碼的技巧
例如_lookUpImpOrForward,
匯編中查找 C/C++方法時药蜻,需要將匯編調(diào)用的方法_lookUpImpOrForward去掉一個下劃線

從_objc_msgSend_uncached過來時behavior 是LOOKUP_INITIALIZE | LOOKUP_RESOLVER,該枚舉定義是

/* method lookup */
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_CACHE = 4,
    LOOKUP_NIL = 8,
};

搜索找到lookUpImpOrForward的定義替饿,主要的步驟分析我都寫在注釋里了语泽。

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //_objc_msgForward_impcache獲取消息轉(zhuǎn)發(fā)的函數(shù)地址,用于方法未找到并且動態(tài)方法決議未處理時調(diào)用视卢。如果消息轉(zhuǎn)發(fā)也不成功踱卵,則程序會拋出方法未識別的錯誤
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    //當(dāng)behavior&LOOKUP_CACHE==1時,查找下緩存据过。為了防止多線程操作時惋砂,調(diào)用函數(shù)時,此時另一線程緩存進(jìn)來了绳锅,可以快速查找一遍緩存西饵,因?yàn)橄旅娴穆俨檎液芎臅r
    //1.當(dāng)從MethodTableLookup過來時,behavior=3鳞芙,3&4=0為假眷柔,往下執(zhí)行
    //2.動態(tài)方法決議回來第一次時'sel=resolveInstanceMethod',此時behavior=12 12&4=4為真原朝,這時還未緩存resolveInstanceMethod闯割,imp為空,往下執(zhí)行)
    //3. 動態(tài)方法決議回來第二次時'sel=say666',此時behavior=12竿拆, 12&4=4為真,無論動態(tài)方法決議流程+resolveInstanceMethod里是否正確添加了imp宾尚,此時緩存中依舊為空丙笋,往下執(zhí)行
    //4.動態(tài)方法決議處理后谢澈,會從resolveMethod_locked的lookUpImpOrForward重新進(jìn)來一次,此時behavior=5御板,5&4=4為真锥忿,此時找到的imp為動態(tài)方法決議第二次進(jìn)來時緩存的say666的方法地址,跳轉(zhuǎn)done_nolock(不緩存)
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    //在isRealized和isInitialized檢查期間保持鎖怠肋,防止對并發(fā)實(shí)現(xiàn)的競爭
    //runtimeLock在方法搜索過程中被保持
    //方法查找+緩存填充相對于方法添加而言是原子的
    //保證方法查找過程中method-lookup + cache-fill中方法添加的原子性敬鬓。
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.
    runtimeLock.lock();

    // TODO: this check is quite costly during process startup.
    //檢查是否是已知類(已經(jīng)加載的類)
    //確保這個類是通過objc_duplicateClass, objc_initializeClassPair或objc_allocateClassPair合法注冊的,或者內(nèi)置到二進(jìn)制文件中的笙各,而不是創(chuàng)建一個看起來像類而實(shí)際并不是的二進(jìn)制的blob钉答,做CFI攻擊
    checkIsKnownClass(cls);
    
    //小概率情況下,當(dāng)前類未實(shí)現(xiàn)時
    //如果沒有杈抢,需要先實(shí)現(xiàn)数尿,目的是為了能確定父類繼承鏈和元類繼承鏈,后面查找遞歸惶楼,當(dāng)前類沒找到右蹦,則需要從繼承鏈查找
    //內(nèi)部有雙向鏈表會更新子類和父類
    //還會把類的相關(guān)屬性協(xié)議方法貼到rw.ext()中
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
    //判斷如果類沒有初始化,則會遞歸父類繼承鏈執(zhí)行初始化所有的類
    //沒實(shí)現(xiàn)時會callInitialize對類發(fā)送消息調(diào)用initialize方法
    //所以側(cè)面說明了initialize和load方法一樣歼捐,不需要主動調(diào)用何陆,底層實(shí)現(xiàn)時幫你調(diào)用
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu 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().
    
    //重點(diǎn)!1ⅰ4ぁ!從這里對imp賦值
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        //查找curClass方法列表颂翼,如果有晃洒,就不需要去找curClass的父類了
        //1.當(dāng)動態(tài)方法決議回來第一次時'sel=resolveInstanceMethod',如果curClass(或者遞歸的父類鏈)方法列表實(shí)現(xiàn)了朦乏,則賦值imp球及,執(zhí)行done
        //2.當(dāng)動態(tài)方法決議回來第二次時'sel=say666',此時curClass的方法列表可以找到imp呻疹,跳到done
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        //如果找到了吃引,則跳轉(zhuǎn)到done(緩存方法、解鎖)
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //本類沒找到刽锤,則將curClass賦值為當(dāng)前類的父類
        //如果父類是nil镊尺,也就是繼承鏈走完了,那么會imp = forward_imp走消息轉(zhuǎn)發(fā)并思,跳出循環(huán)
        if (slowpath((curClass = curClass->superclass) == 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.
        //如果父類繼承鏈中存在環(huán)庐氮,就停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        //查找當(dāng)前類的緩存,注意宋彼,上面已經(jīng)將curClass父類賦值給當(dāng)前類弄砍,所以是查找父類的緩存仙畦,并不是遞歸當(dāng)前類了
        //查找過程是`cache_getImp` - `lookup` - `lookUpImpOrForward`,也是通過`cache_getImp`查找緩存音婶,不過參數(shù)是`GETIMP`慨畸,跟上面NORMAL區(qū)別是找不到緩存時,不是走`_objc_msgSend_uncached`衣式,而是走`LGetImpMiss`寸士,最后會返回nil,避免了導(dǎo)致死循環(huán)碴卧。
        imp = cache_getImp(curClass, sel);
        //上面當(dāng)父類鏈的父類為nil時弱卡,imp = forward_imp進(jìn)行賦值,所以走下面這一步說明了父類鏈已經(jīng)走完都沒找到螟深。這時跳出循環(huán)谐宙,首先會調(diào)用該類的方法解析器
        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;
        }
        //如果找到了父類imp,則跳轉(zhuǎn)done(緩存方法界弧、解鎖)凡蜻,緩存在curClass(當(dāng)次循環(huán)中curClasss是入?yún)㈩悇t緩存到入?yún)㈩悾悄臣壐割惥途彺娴侥臣壐割愔校?        //1.當(dāng)動態(tài)方法決議回來第一次時'sel=resolveInstanceMethod'垢箕,這時候從cache找的imp是空的划栓,不走進(jìn)這里。
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
        //當(dāng)前父類沒有找到条获,由于for循環(huán)沒有跳出的條件判斷忠荞,會一直死循環(huán)查找父類繼承鏈,直到break
    }

    // No implementation found. Try method resolver once.
    //這一步代表了流程只走一次下面這段代碼(也就是只會走一次動態(tài)方法決議)帅掘,因?yàn)?behavior ^= LOOKUP_RESOLVER改變了behavior委煤,與條件behavior & LOOKUP_RESOLVER = 1不能同時滿足
    //LOOKUP_RESOLVER =2,behavior & LOOKUP_RESOLVER也就是3&2=2修档,執(zhí)行條件語句代碼塊
    //1.從CachLookup后進(jìn)來behavior=3碧绞,3&2為真
    //2.從動態(tài)方法決議回來第一次:12,此時sel為resolveInstanceMethod吱窝,12&2為假
    //3.從動態(tài)方法決議回來第二次:12讥邻,此時sel為say666,12&2為假
    //也就是保證了動態(tài)方法決議只會被執(zhí)行一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //behavior = 3^2 = 1
        behavior ^= LOOKUP_RESOLVER;
        //沒有找到imp院峡,則開始走本類的動態(tài)方法決議
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    //緩存方法
    //當(dāng)從入?yún)㈩惢蛘吒割愭溨姓业絠mp兴使,則緩存sel和imp到當(dāng)前循環(huán)中的curClass中。1.當(dāng)從_objc_msgSend_uncached通過NORMAL進(jìn)來直接在入?yún)㈩?父類繼承鏈的方法列表中查找到imp時照激,此時sel和imp均為say666 1.如果是動態(tài)方法決議第一次進(jìn)來发魄,sel='resolveInstanceMethod',imp為resolveInstanceMethod的實(shí)現(xiàn)地址 2.如果是動態(tài)方法決議第二次進(jìn)來俩垃,此時sel='say666'欠母,imp為動態(tài)方法決議添加的say666的方法地址
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    //解鎖
    runtimeLock.unlock();
 done_nolock:
    //當(dāng)動態(tài)方法決議處理完成欢策,從resolveMethod_locked的lookUpImpOrForward重新進(jìn)來,此時behavior=5赏淌,5&8=0為假,執(zhí)行最后的return imp啄清。此時的imp是動態(tài)方法決議第二次進(jìn)來時緩存的say666的方法地址六水。就這樣如果動態(tài)方法決議階段+resolveInstanceMethod里正確添加了sel的imp,那在這一步就找到了imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    //當(dāng)動態(tài)方法決議整個流程都沒找到imp時辣卒,那么此時的imp就是forward_imp掷贾,也就是_objc_msgForward_impcache,回到MethodTableLookup荣茫,把imp的值給x17想帅,接著TailCallFunctionPointer x17,也就是調(diào)用默認(rèn)的轉(zhuǎn)發(fā)處理_objc_msgForward_impcache這個方法啡莉,進(jìn)入消息轉(zhuǎn)發(fā)流程港准。如果依然未處理,則拋出消息未識別的錯誤咧欣,終止進(jìn)程浅缸。
    return imp;
}

主要步驟是:

  • 【第一步】由于多線程可能導(dǎo)致方法緩存在另外線程添加,為了避免下面慢速查找繼承鏈方法列表需要消耗大量時間魄咕,所以再次從cache緩存中進(jìn)行查找衩椒,即快速查找,找到則直接返回imp哮兰,否則毛萌,則進(jìn)入【第二步】

  • 【第二步】判斷cls是不是已知類

    • 如果不是,則報錯

    • 類是否已實(shí)現(xiàn)喝滞,如果沒有阁将,則需要先實(shí)現(xiàn),確定其父類鏈或元類繼承鏈囤躁,此時實(shí)例化的目的是為了確定父類鏈冀痕、ro、以及rw等狸演,方法后續(xù)數(shù)據(jù)的讀取以及查找需要

    • 是否已初始化initialize言蛇,如果沒有,則初始化

  • 【第三步】for循環(huán)宵距,按照類繼承鏈 或者 元類繼承鏈的順序查找

    • 當(dāng)前類的方法列表中使用二分查找算法查找方法腊尚,如果找到,則將方法寫入cache(在iOS底層之cache_t探究中分析了寫入過程)满哪,并返回imp婿斥,如果沒有找到劝篷,則返回nil

    • 當(dāng)前cls被賦值為父類,如果父類等于nil民宿,則imp = forward_imp娇妓,并跳出循環(huán),進(jìn)入【第四步】

    • 如果父類鏈中存在循環(huán)活鹰,則報錯哈恰,終止循環(huán)

    • 從父類緩存中查找方法

      • 如果未找到,進(jìn)入下一次循環(huán)志群,Method meth = getMethodNoSuper_nolock(curClass, sel);着绷,curClass變?yōu)槠涓割悾檎腋割惙椒斜?/p>

      • 如果找到锌云,則直接返回imp荠医,執(zhí)行cache寫入流程

  • 【第四步】判斷是否執(zhí)行過動態(tài)方法決議

    • 如果沒有,執(zhí)行動態(tài)方法決議

其中部分函數(shù)的源碼

  1. 二分法查找方法列表的imp
  • getMethodNoSuper_nolock查找當(dāng)前類的方法列表
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    //查找data里的methods() 方法列表
    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;
}

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
  • 重點(diǎn)IO选彬向!上面的函數(shù),重點(diǎn)在于findMethodInSortedMethodList(sel, mlist);以二分法查找
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    //list為data()中的方法列表
    ASSERT(list);

    const method_t * const first = &list->first;
    //取base為第一個元素
    const method_t *base = first;
    const method_t *probe;
    //key為傳進(jìn)來的cmd石洗,也就是我們調(diào)用的sayHello
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    //count賦值為列表元素個數(shù)幢泼,每一次循環(huán)都將count/2
    for (count = list->count; count != 0; count >>= 1) {
        //base為二分法區(qū)間的最小元素。將base平移count/2讲衫,也就是平移到列表中間的位置茴扁,probe為中間的元素
        probe = base + (count >> 1);
        //獲取probe存儲的名字饿肺,也就是sel方法名
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        //如果查找的cmd等于sel
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            //由于分類的重寫的方法加載到內(nèi)存中是會插在類的同名方法前面,所以這里需要循環(huán)向前一個查找sel,如果有比驻,則返回分類的sel锌畸。
            //如果存在多個分類重寫敷硅,則看哪個分類先加載
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        //如果cmd大于sel怎棱,則需要往后面查找,賦值最小值base為中間值probe+1拥诡,如果cmd小于sel触趴,則不走進(jìn)來
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
        //沒找到一致的sel,則進(jìn)入下一次循環(huán)渴肉,再取base和count的中間值
    }
    //如果循環(huán)到count==0冗懦,也沒找到,則返回nil
    return nil;
}

其執(zhí)行流程圖如下


二分法查找方法列表流程
  1. 查找父類imp源碼:
    _cache_getImp入口-> CacheLookup (GETIMP)
STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp

開始CacheLookup (GETIMP)仇祭,如果命中則CacheHit披蕉,未命中則CheckMiss 或者JumpMiss ->

.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LLookupRecover$1 which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif


    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

CheckMiss / JumpMiss -> LGetImpMiss,則執(zhí)行LGetImpMiss

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP //走這個分支
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

其流程圖:


父類緩存查找流程圖.png
  1. 如果父類鏈沒找到imp,則進(jìn)入動態(tài)方法決議resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    //lookUpImpOrForward沒找到imp没讲,再給你一次機(jī)會眯娱,去處理
    runtimeLock.unlock();
    //如果不是元類
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        //直接走當(dāng)前類的動態(tài)方法決議
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //元類,走元類的動態(tài)方法決議
        resolveClassMethod(inst, sel, cls);
        //再查找一次methodList爬凑,如果還是nil徙缴,會查找一次當(dāng)前類的動態(tài)方法決議
        if (!lookUpImpOrNil(inst, sel, cls)) {
            //走當(dāng)前類的動態(tài)方法決議
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //上面給你機(jī)會去處理,現(xiàn)在重新找一次lookUpImpOrForward贰谣,此時behavior | LOOKUP_CACHE = 1|4 = 5
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //如果resolveInstanceMethod方法未實(shí)現(xiàn)娜搂,則直接退出,這一步會觸發(fā)一次查找lookUpImpOrNil-> lookUpImpOrForward吱抚,這時的behavior為0|LOOKUP_CACHE | LOOKUP_NIL=4|8=12
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

看看lookUpImpOrNil的實(shí)現(xiàn)

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

先調(diào)用了resolveInstanceMethod去看看resolver中幫沒幫實(shí)現(xiàn),如果幫實(shí)現(xiàn)了考廉,在走lookUpImpOrNil過程中就會把方法緩存起來秘豹。然后會回到resolveMethod_locked方法中調(diào)用lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE),這時可以從緩存找到imp昌粤,進(jìn)入done_nolock, 返回方法的imp既绕;如果整個慢速查找流程(包括動態(tài)方法決議)都沒有找到,就會返回存著forward_impimp涮坐。然后就又會回到MethodTableLookup凄贩,把imp的值給x17,接著TailCallFunctionPointer x17袱讹,調(diào)用forward_imp也就是_objc_msgForward_impcache這個方法疲扎,進(jìn)去消息轉(zhuǎn)發(fā)流程。

元類的動態(tài)方法決議流程和上面類實(shí)例的流程差不多捷雕,這里不再做分析椒丧。resolveClassMethod的實(shí)現(xiàn)是

* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

為什么查找元類方法時在執(zhí)行完_class_resolveClassMethod之后會再次執(zhí)行一次_class_resolveInstanceMethod?

比如有一個類Person救巷,我們查找 Person 的一個類方法壶熏,如果沒找到,會繼續(xù)找他的第一個元類浦译,再找不到棒假,會繼續(xù)找根元類 ,最終會找到 NSObject精盅。
Person(類方法) 找——> 元類(實(shí)例方法) 找——> 根元類(實(shí)例方法) 找——> NSObject(實(shí)例方法)
實(shí)例方法存在類對象里面帽哑,類方法存在元類里面。
當(dāng)遍歷元類繼承鏈后都沒找到時渤弛,就找一遍NSObject的實(shí)例方法列表祝拯。


isa走向.png
  1. 下面來看看_objc_msgForward_impcache做了什么__objc_msgForward_impcache
STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    //進(jìn)入__objc_msgForward
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward
    //把存有__objc_forward_handler的頁存入X17
    adrp    x17, __objc_forward_handler@PAGE
    //把x17向后偏移了__objc_forward_handler@PAGEOFF,然后把X17中地址的值存入p17,
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

TailCallFunctionPointer是個方法指針佳头,調(diào)用了上面的__objc_forward_handler

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

由于查找__objc_forward_handler未找到匯編定義鹰贵,去掉一個_查找C/C++源碼:

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

可以看到是由objc_defaultForwardHandler定義的。而這個函數(shù)里面康嘉,我們看到了熟悉的方法選擇器未找到的打印信息碉输。從打印信息的組成class_isMetaClass(object_getClass(self)) ? '+' : '-',可以看到亭珍,系統(tǒng)內(nèi)部并沒有區(qū)分方法是實(shí)例方法還是類方法敷钾,而是通過是否是元類,來區(qū)分打印方法類型肄梨。
其流程圖:

然而在報錯之前阻荒,還會對消息進(jìn)行轉(zhuǎn)發(fā)和方法重簽名調(diào)用,再次挽救众羡。加上動態(tài)方法決議侨赡,共有3次挽救機(jī)會。

消息轉(zhuǎn)發(fā)機(jī)制

這個流程留待下一節(jié)分析粱侣。

總結(jié)

查找實(shí)例方法羊壹,會在中查找,慢速查找的繼承鏈?zhǔn)牵?code>類->父類->根類->nil

同理齐婴,查找類方法油猫,則在元類中查找,其慢速查找的鏈?zhǔn)牵?code>元類->根元類->根類->nil

如果objc_msgSend快速查找緩存柠偶,慢速查找當(dāng)前類方法列表情妖、父類的緩存和方法列表都沒有找到imp方法實(shí)現(xiàn),則嘗試動態(tài)方法決議
如果動態(tài)方法決議仍然沒有找到嚣州,則進(jìn)行消息轉(zhuǎn)發(fā)
如果消息轉(zhuǎn)發(fā)還沒有處理鲫售,則進(jìn)去方法簽名處理調(diào)用
如果方法簽名處理調(diào)用還未處理该肴,則程序會拋出錯誤情竹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市匀哄,隨后出現(xiàn)的幾起案子秦效,更是在濱河造成了極大的恐慌,老刑警劉巖涎嚼,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阱州,死亡現(xiàn)場離奇詭異,居然都是意外死亡法梯,警方通過查閱死者的電腦和手機(jī)苔货,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進(jìn)店門犀概,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夜惭,你說我怎么就攤上這事姻灶。” “怎么了诈茧?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵产喉,是天一觀的道長。 經(jīng)常有香客問我敢会,道長曾沈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任鸥昏,我火速辦了婚禮塞俱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吏垮。我一直安慰自己敛腌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布惫皱。 她就那樣靜靜地躺著,像睡著了一般尤莺。 火紅的嫁衣襯著肌膚如雪旅敷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天颤霎,我揣著相機(jī)與錄音媳谁,去河邊找鬼。 笑死友酱,一個胖子當(dāng)著我的面吹牛晴音,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缔杉,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼锤躁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了或详?” 一聲冷哼從身側(cè)響起系羞,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霸琴,沒想到半個月后椒振,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梧乘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年澎迎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡夹供,死狀恐怖灵份,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罩引,我是刑警寧澤各吨,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站袁铐,受9級特大地震影響揭蜒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剔桨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一屉更、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洒缀,春花似錦瑰谜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饺饭,卻和暖如春渤早,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘫俊。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工鹊杖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扛芽。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓骂蓖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親川尖。 傳聞我的和親對象是個殘疾皇子登下,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評論 2 348