objc_msgSend流程分析之快速查找

在上一篇文章中,我們了解了cache的寫入流程,那么是怎么進(jìn)行方法的查找呢,接下來我們在這篇以及下面的文章來進(jìn)行探討,本篇文章先對方法的快速查找進(jìn)行分析。

在分析之前,先來說一下RuntimeRuntime是給OC這門對象語言提供的運(yùn)行時(shí),是通過底層的C,C++和匯編實(shí)現(xiàn)的,其中Runtime的使用方式有三種:

第一種:通過OC代碼,例如 [person sayNB]

第二種:通過NSObject里面的方法,例如isKindOfClass

第三種:通過Runtime API,例如class_getInstanceSize

說到這里, 我覺得附上一張關(guān)系圖能讓你們更加清晰

Runtime三種方式及底層的關(guān)系

了解Runtime之后,我們接下來玩一個(gè)有意思的東西

clang編譯后的底層實(shí)現(xiàn)

我們把代碼編譯后,來到 clang,把里面的代碼實(shí)現(xiàn)拿到main函數(shù)里面,運(yùn)行,發(fā)現(xiàn)結(jié)果和OC的上層調(diào)用方法是一摸一樣的,從而可以看出,我們的方法到了底層之后,就是 通過objc_msgSend消息發(fā)送的

也可以通過msg搜索把Enable Strict Checking of objc_ msgSend Calls嚴(yán)厲機(jī)制關(guān)了,用objc_msgSend來發(fā)送消息,就像這樣

通過objc_msgSend發(fā)送消息

效果等同于[person1 sayNB];,除了這一點(diǎn),我們還可以用person1的調(diào)用執(zhí)行父類中實(shí)現(xiàn),通過objc_msgSendSuper來實(shí)現(xiàn)

給父類發(fā)送消息

在這個(gè)過程中,我們的子類是沒有實(shí)現(xiàn) sayhello的,所以從中可以看出 [person sayHello]objc_msgSendSuper執(zhí)行的都是父類中的 sayHello

那么問題來了,objc_msgSend是怎么找到我們的方法的呢? 現(xiàn)在我們明白了OC上層調(diào)用的方法,到了下層是發(fā)送消息。 消息里有selimp,通過sel方法編號綁定imp,imp就是我們的函數(shù)指針地址,通過imp我們可以找到具體的內(nèi)容。那么我們怎么通過sel找到imp呢, 這就是本篇文章關(guān)注的重點(diǎn)

objc_msgSend 快速查找流程分析

由于C和C++查找的話比匯編慢了一丟丟,為了提高性能和方法的動態(tài)性,selimp是通過匯編找的。聽到匯編不要緊張,不要驚慌,看不懂匯編沒關(guān)系,我會標(biāo)好注釋,我們只需要知道流程即可

我們先找到我們的匯編代碼,通過xcode搜索objc_msgsend,由于是匯編寫的,所以找后綴.s 的文件,然后我們常用的架構(gòu)又是arm64,所以最終找到了objc-msg-arm64.s里面

搜索objc_msgsend

通過 ENTRY _objc_msgSend找到底層的匯編源碼

_objc_msgSend源碼

在講這個(gè)代碼之前,我們要始終記住一點(diǎn),就是我們一直都在通過selimp,記住這一點(diǎn)之后,我們分析代碼的時(shí)候不會迷路

好了,開始我們的匯編分析之旅:

//  _objc_msgSend 入口
    ENTRY _objc_msgSend
    // 無窗口
    UNWIND _objc_msgSend, NoFrame

    // cmp 是對比的意思, p0 是objc_msgSend的第一個(gè)參數(shù)。意思是p0先和空做對比,如果為空就沒有必要往下走了。
    cmp p0, #0          // nil check and tagged pointer check
    // le小于 --支持taggedpointer(小對象類型)的流程
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    // p0 等于 0 時(shí),直接返回 空
    b.eq    LReturnZero
#endif
    //  根據(jù)對象拿出isa
    ldr p13, [x0]       // p13 = isa
    //  根據(jù)isa 拿出類  
    GetClassFromIsa_p16 p13     // p16 = class
// 獲取isa完畢
LGetIsaDone:
    
    // CacheLookup,從緩存里面獲取imp的流程,也就是我們今天探討的重點(diǎn),從sel-imp快速查找流程
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // 等于空,直接返回空
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

到這一步我相信都很容易理解,主要是拿到類信息,即class,拿到class之后來到CacheLookup從緩存里面獲取imp的流程。

我們先來看如何拿到class的,先找到macro GetClassFromIsa_p16,然后看源碼實(shí)現(xiàn)

匯編獲取isa分析

有一說一,#if SUPPORT_INDEXED_ISA里面的這些匯編我也看不太懂,參考了高人的指點(diǎn)。不過我們也不需要看懂,因?yàn)槲覀?code>arm64真機(jī)不會走到那里面去,我們走的是#elif __LP64__里面,這里面就很好理解了, 直接拿isa & ISA_MASK 就可以獲取到類信息,這個(gè)在之前的文章已經(jīng)講過。

好了,獲取class完成后,接下來我們重點(diǎn)分析CacheLookup, 我們首先找到macro CacheLookup,注意,在當(dāng)前類里面找,別找到別人家去了


.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
    // CACHE是一個(gè)宏定義, 2 * 8 = 16 #define CACHE            (2 * __SIZEOF_POINTER__)
    // p16是isa,isa位移16個(gè)字節(jié)得到我們的cache,然而cache的首地址又是mask_buckets
    // 然后把cache又放到p11里面
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

// ------------  arm64位真機(jī)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets,存到p10 = buckets
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets

    // LSR:p11(cache)邏輯右移48位,拿到mask。 然后 mask & p1(sel & mask) ,得到sel-imp的下標(biāo)index(即前面講過的cache_hash的下標(biāo)) 存入p12
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask

// --------- 非arm64位真機(jī) 這些就不用看了
#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


    // p12是獲取的下標(biāo),然后邏輯左移4位,相當(dāng)于是(下標(biāo) * 16(16是sel和imp的大小)),再由p10(buckets)通過內(nèi)存平移的方式得到bucket保存到p12中
    add p12, p10, p12, LSL #(1+PTRSHIFT) = (1 + 3) = 4
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    // 得到bucket之后通過指針地址得到{imp, sel},然后將imp 和 sel分別賦值為p17 和 p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    // 接下來就是開始循環(huán)了, 判斷當(dāng)前bucket的 sel 與 p1(傳入的sel)是否相等
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    // 如果不相同,則跳入2f
    b.ne    2f          //     scan more
    // 如果相同直接返回imp
    CacheHit $0         // call or return imp
    
    // 沒有找到 進(jìn)入2f, 如果一直都找不到, 因?yàn)槭莕ormal ,跳轉(zhuǎn)至__objc_msgSend_uncached
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    // 判斷p12(下標(biāo)對應(yīng)的bucket)是否 等于 p10(buckets數(shù)組第一個(gè)元素),如果等于,則跳轉(zhuǎn)到3f
    cmp p12, p10        // wrap if bucket == buckets
    // 如果相等 跳入3f
    b.eq    3f
    // 因?yàn)橐獙12的指針指到buckets的最后一個(gè)元素后,所以進(jìn)行了bucket--, 向前一直查找下去
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    // 跳轉(zhuǎn)至第1步,遞歸繼續(xù)對比 sel 與 cmd
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16

    // p11(mask)右移44位 相當(dāng)于mask左移4位,直接定位到buckets的最后一個(gè)元素
    // 注意,這個(gè)地方會往下面走,不會再回去了
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
        
                    // 把當(dāng)前查詢的bucket人為設(shè)為最后一個(gè)元素給了p12,然后會往下走
                    // p12 = buckets + (mask << 1+PTRSHIFT)    

// 這個(gè)地方不是真機(jī)環(huán)境,不用看
#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.


    // 上面步驟之后然后在繼續(xù)查找,拿到x12(即p12)bucket中的 imp-sel 分別存入 p17-p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket

    // 比較 sel 與 p1(傳入的參數(shù)cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    // 如果不相等,即走到2f
    b.ne    2f          //     scan more
    // 如果相等 即命中,直接返回imp
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    // 如果一直找不到,則CheckMiss
    CheckMiss $0            // miss if bucket->sel == 0
    // 判斷p12(bucket)是否 等于 p10(buckets數(shù)組第一個(gè)元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
    cmp p12, p10        // wrap if bucket == buckets
    // 如果等于,跳轉(zhuǎn)至第3步
    b.eq    3f
    // 從p12 buckets首地址 - 實(shí)際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個(gè)bucket元素,imp-sel分別存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    // 跳轉(zhuǎn)至第1步,繼續(xù)對比 sel 與 cmd
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    // 跳轉(zhuǎn)至JumpMiss 因?yàn)槭莕ormal ,跳轉(zhuǎn)至__objc_msgSend_uncached

    JumpMiss $0

.endmacro

看完這一大篇匯編代碼,有的同學(xué)可能不太明白,我也是費(fèi)了老大功夫?qū)戇@個(gè)注釋??床欢畢R編沒關(guān)系,流程弄明白再百度匯編語法慢慢看。

我們來總結(jié)一下,我相信我總結(jié)的能夠讓你更加清晰整個(gè)流程,把流程弄清楚再來看匯編也許感覺就不一樣了。

主要分為以下六步:
1、拿到類的isa之后通過內(nèi)存平移16個(gè)字節(jié),找到我們的cache,就是匯編中的p11

2、從cache里面取出bucketsmask,通過cmd & mask 拿到哈希下標(biāo)index存入p12

3、根據(jù)所得的哈希下標(biāo)indexbuckets內(nèi)存平移,取出哈希下標(biāo)對應(yīng)的bucket

4、 判斷bucketsel傳入的參數(shù)cmd是否相等,如果相等直接返回imp,如果不相等則判斷是否是第一個(gè)元素,如果是,把當(dāng)前bucket人為設(shè)為最后一個(gè)元素,進(jìn)入到第五個(gè)步驟。 如果不是則{imp, sel} = *--bucket,遞歸向前查找回到第四個(gè)步驟,繼續(xù)進(jìn)行對比

注意,人為設(shè)定到最后一個(gè)元素只有一次,設(shè)定完就會到第五個(gè)步驟

5、找到了第一個(gè)元素,人為設(shè)定到最后一個(gè)元素之后:再遞歸向前查找:比較 sel傳入的參數(shù)cmd 是否相等,如果不相等,繼續(xù)向前查找,直到找到sel等于cmd,返回imp

6、 如果步驟4步驟5兩次遞歸完了后還沒找到則會跳到__objc_msgSend_uncached,進(jìn)入慢速查找流程,我們下篇文章再進(jìn)行分析。

最后,再附上一張objc_msgSend快速查找流程圖結(jié)束今天的內(nèi)容

objc_msgSend快速查找流程圖

iOS 底層原理 文章匯總

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容