objc_msgSend消息快速查找

消息發(fā)送之快速轉(zhuǎn)發(fā)

在之前文章objc_class中的cache_t分析中,我們分析了,cache_t存儲方法的過程,有留下一個疑問:cache_t在存儲方法之前有讀取方法的,那它是怎么讀取方法的呢?

首先,讓我們先了解,什么是方法,方法的本質(zhì)是什么。顧名思義,類或者對象調(diào)用一個API,這個API就是方法,平時我們接觸太多了,有自己定義的方法,也有調(diào)用系統(tǒng)的方法,那么這個方法在底層到底是什么?在何時調(diào)用的。

一、方法的本質(zhì)
1.1 方法是何時調(diào)用

  1. 創(chuàng)建一個student類,Teacher類,student繼承Teacher。在student里聲明sayStu方法,但沒有實現(xiàn)。> 編譯時和運行時圖

  2. 依次編譯(command+B)、運行(command+R),結(jié)果:編譯是成功的,運行時是崩潰報錯的>

    方法沒實現(xiàn)@2x.png

  3. 可以得知我們方法都是在運行時執(zhí)行的。

    補充:編譯時運行時概念
    編譯時:編譯器把源代碼翻譯成機器可以識別的代碼過程。簡單的說,是個翻譯的工作,檢查代碼里有沒有錯寫的關(guān)鍵字、詞法分析、語法分析之類靜態(tài)類型檢查
    運行時:簡單得說,就是代碼跑起來,被裝載到內(nèi)存里面去(代碼保存在磁盤上沒有裝入到內(nèi)存之前是個死的東西,只有到內(nèi)存里才是的),如果此時出錯,則程序會崩潰,是一個動態(tài)階段。
    OC的運行時機制:一是將數(shù)據(jù)類型的確定由編譯時,推遲到運行時。OC的這種運行時機制使對象的類型及對象的屬性和方法在運行時才能確定;二是讓OC具備多態(tài)(不同對象以自己的方式響應(yīng)相同的消息)特性
    Runtime的三種調(diào)用oc代碼調(diào)用(如自定義方法)、framework調(diào)用(系統(tǒng)方法) 、RuntimeAPI(如class_getInstanceSize)調(diào)用

runtime三種方法調(diào)用.png

1.2 方法在底層是如何實現(xiàn)的

  • 下面我們給student加上sayStu的實現(xiàn),再clang一下main.m文件,看下底層方法是如何調(diào)用的

    clang(是一個由Apple主導(dǎo)編寫,基于LLVM的C/C++/OC的編譯器):clang -rewrite-objc main.m -o main.cpp
    搜索sayStu,可以觀察到,其實是調(diào)用了objc_msgSend函數(shù),而且alloc方法也調(diào)用了objc_msgSend函數(shù)

    方法的本質(zhì)@2x.png

  • 調(diào)用格式:objc_msgSend: (消息接收者, 消息主體)

  • 嘗試在main文件里使用objc_msgSend調(diào)用方法
    需要導(dǎo)入頭文件#import <objc/message.h>
    手動關(guān)閉運行時的編譯警告: buildSettings->Enable Strict Checking of objc_msgSend Calls->設(shè)置為No(對objc_msgSend調(diào)用的嚴(yán)格檢查關(guān)閉)
    加入測試代碼objc_msgSend(p, sel_registerName("sayStu"));
    打印結(jié)果

    三種方式調(diào)用@2x.png

    我們發(fā)現(xiàn),是調(diào)用成功的,三種形式的調(diào)用方式都是一個效果。
    由此我們可以得出結(jié)論:方法的本質(zhì)就是方法名和對應(yīng)的函數(shù)代碼
    oc層面:是對象/類+方法名
    底層層面:是objc_msgSend發(fā)送消息sel->通過sel(方法編號)找到imp(函數(shù)指針地址) -> 找到函數(shù)內(nèi)容
    上述流程我們唯一不清楚的是怎么通過sel找到imp的
    下面就分析方法編號是如何綁定函數(shù)指針地址的

二、objc_msgSend解析

  • 打開objc源碼,搜索objc_msgsend,發(fā)現(xiàn)objc_msgsend是匯編語言寫的。原因是:一是整個程序方法很多,調(diào)用極其頻繁的,所以要求速度快。二是參數(shù)的不確定性,如果用C/C++,它們是靜態(tài)性的,速度會很慢。總體來說還是為了性能更好
  • 還有一個疑問是,objc_msgSend為什么需要需要傳入?yún)?shù)消息接受者,這是因為消息接受者里有isaisa->類/元類->cache_t(方法緩存)->methodlist(存在bits里)
  • imp的查找分為2個階段,快速查找(從緩存cache中查找,匯編語言編寫)和慢速查找(方法列表methodTable中,c/c++編寫),今天先分析快速查找
  • 打開objc源碼,搜索objc_msgsend,選擇arm64真機環(huán)境進(jìn)行探索(其他環(huán)境也是類似),找到objc_msgSend入口
ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
//p0 和空對比,即判斷接收者是否存在,其中p0是objc_msgSend的第一個參數(shù)-消息接收者receiver
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS//支持taggedpointer(小對象類型)的流程
    b.le    LNilOrTagged//判斷是否是小對象或者為nil,則跳轉(zhuǎn)到LNilOrTagged//  (MSB tagged pointer looks negative)//
#else
    b.eq    LReturnZero//p0 等于 0 時,直接返回 空
#endif
//p0即receiver 肯定存在的消息的流程
//根據(jù)對象內(nèi)存,首地址是isa,拿出isa ,即從x0寄存器指向的地址 取出 isa,存入 p13寄存器
    ldr p13, [x0]       // p13 = isa
//p16 = isa(p13) & ISA_MASK,得到class
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone://獲取完畢
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend//在緩存中尋找IMP

以上步驟: 1、判斷objc_msgSend 的第一個參數(shù),即接受者是否為空。如果為空:則跳轉(zhuǎn)到 LReturnZero流程,賦值為空。如果是TAGGED_POINTERS(小對象:如nsstring、nsnumber等類型,它們本身就是值,),則跳轉(zhuǎn)到LNilOrTagged,如果小對象為空,則也跳轉(zhuǎn)到LReturnZero,賦值為空;如果不為空,則跳轉(zhuǎn)步驟2。如果既不為空,也不是小對象,則取出isa,存入p13寄存器,執(zhí)行GetClassFromIsa_p16:也是步驟2
2、

#if SUPPORT_INDEXED_ISA
    .align 3
    .globl _objc_indexed_classes
_objc_indexed_classes:
    .fill ISA_INDEX_COUNT, PTRSIZE, 0
#endif

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
//將isa的值存入p16寄存器
    mov p16, $0         // optimistically set dst = src
//判斷是否是 nonapointer isa
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
//將_objc_indexed_classes所在的頁的基址 讀入x10寄存器 :
    adrp    x10, _objc_indexed_classes@PAGE
//x10基址 根據(jù) 偏移量 進(jìn)行 內(nèi)存偏移:x10 = x10 + _objc_indexed_classes(page中的偏移量)
    add x10, x10, _objc_indexed_classes@PAGEOFF
//從p16的第ISA_INDEX_SHIFT位開始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0補充
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
//將x10+p16PTRSHIFT字節(jié)存到p16中
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__//用于64位系統(tǒng)
    // 64-bit packed isa
//p16 = isa & ISA_MASK(位運算 & 即獲取isa中的shiftcls信息)= class
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
//用于32位系統(tǒng)
    mov p16, $0

#endif

.endmacro

3、第二步結(jié)束后,就執(zhí)行到CacheLookup NORMAL, _objc_msgSend(快速查找流程),搜索CacheLookupobjc-msg-arm64.s匯編文件中搜索CacheLookup,找到.macro CacheLookup定義處

.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 --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 從x16(即isa)中平移16字節(jié),取出cache 存入p11寄存器 -- isa距離cache 正好16字節(jié):isa(8字節(jié))-superClass(8字節(jié))-cache(mask高16位 + buckets低48位)
    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
////---- 64位真機
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下buckets
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
//--- p11(cache)右移48位,得到mask(即p11 存儲mask),mask & p1(msgSend的第二個參數(shù) cmd-sel) ,得到sel-imp的下標(biāo)index(即搜索下標(biāo)) 存入p12(cache insert寫入時的哈希下標(biāo)計算是 通過 sel & mask,讀取時也需要通過這種方式)
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
//--- 非64位真機
#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) p10是buckets數(shù)組首地址,下標(biāo) * 1<<4(即16) 得到實際內(nèi)存的偏移量,通過buckets的首地址偏移,獲取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 實際含義就是得到一個bucket占用的內(nèi)存大小 -- 相當(dāng)于mask = occupied -1-- _cmd & mask -- 取余數(shù)
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//--- 從x12(即p12)中取出 bucket 分別將imp和sel 存入 p17(存儲imp) 和 p9(存儲sel)
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
////--- 比較 sel 與 p1(傳入的參數(shù)cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)
////--- 如果不相等,即沒有找到,請?zhí)D(zhuǎn)至 2f
    b.ne    2f          //     scan more
//--- 如果相等 即cacheHit 緩存命中,直接返回imp
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
//--- 如果一直都找不到,則CheckMiss, 因為是normal ,跳轉(zhuǎn)至__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
//--- 判斷p12(下標(biāo)對應(yīng)的bucket) 是否 等于 p10(buckets數(shù)組第一個元素,),如果等于,則跳轉(zhuǎn)至第3步
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
//-- 從x12(即p12 buckets首地址)- 實際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
    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
//--- 設(shè)置到最后一個元素
//--- p11(mask)右移44位 相當(dāng)于mask左移4位,直接定位到buckets的最后一個元素,緩存查找順序是向前查找
    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.
//--- 再查找一遍緩存()
//--- 拿到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)
//--- 如果不相等,即走到第二步
    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(下標(biāo)對應(yīng)的bucket) 是否 等于 p10(buckets數(shù)組第一個元素)-- 表示前面已經(jīng)沒有了,但是還是沒有找到
    cmp p12, p10        // wrap if bucket == buckets
////如果等于,跳轉(zhuǎn)至第3步
    b.eq    3f
//--- 從x12(即p12 buckets首地址)- 實際需要平移的內(nèi)存大小BUCKET_SIZE,得到得到第二個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 因為是normal ,跳轉(zhuǎn)至__objc_msgSend_uncached
    JumpMiss $0

.endmacro

以上其中CacheLookup NORMAL流程主要分為以下幾個步驟

  • 通過類首地址平移16字節(jié)(因為在objc_class中,首地址距離cache正好16字節(jié),isa占8字節(jié),superClass占8字節(jié)),得到cahce,cache中高16位存mask,低48位存buckets,即p11 = cache
  • cache中分別取出buckets和mask,并由mask根據(jù)哈希算法計算出哈希下標(biāo);通過cache和掩碼(即0x0000ffffffffffff(二進(jìn)制是前16位為0,后48位為1)的 & 運算,將高16位mask抹零,得到buckets指針地址,即p10 = buckets
  • 將cache右移48位,得到mask,即p11 = mask
  • objc_msgSend的參數(shù)p1(即第二個參數(shù)_cmd)& mask,通過哈希算法,得到需要查找存儲sel-impbucket下標(biāo)index,即p12 = index = _cmd & mask,用這個算法,是因為在存儲sel-imp時,也是通過同樣哈希算法計算哈希下標(biāo)進(jìn)行存儲的。
  • 根據(jù)下標(biāo),在buckets里根據(jù)index*16內(nèi)存偏移(一個bucket的大小)得到對應(yīng)存儲的bucket,并拿出其中的sel(p9)和_cmd進(jìn)行比較, 如果相等,說明命中了,則跳轉(zhuǎn)到CacheHit,尋找imp
緩存命中@2x.png

如果不相等,有兩種情況
一、如果一直都找不到,直接跳轉(zhuǎn)至CheckMiss,因為是normal,會跳轉(zhuǎn)至__objc_msgSend_uncached,進(jìn)入慢速查找流程
二、【1】如果根據(jù)當(dāng)前獲取到的bucket 等于 buckets的第一個元素,則人為的將當(dāng)前bucket設(shè)置為buckets的最后一個元素(通過buckets首地址+mask右移44位(左移4位)直接定位到buckets的最后一個元素),然后繼續(xù)進(jìn)行遞歸循環(huán),即【3】
【2】如果當(dāng)前bucket不等于buckets的第一個元素,則p12向前移一個bucket位置,進(jìn)入第一次遞歸循環(huán)。
【3】第二次遞歸循環(huán):重復(fù)【2】的操作,如果當(dāng)前的bucket還是等于 buckets第一個元素,則直接跳轉(zhuǎn)至JumpMiss,到__objc_msgSend_uncached,也進(jìn)入慢速查找流程
CheckMissJumpMiss具體實現(xiàn)如下,

緩存沒命中@2x.png

都進(jìn)入到__objc_msgSend_uncached,即慢速查找流程
具體實現(xiàn)

methodlist@2x.png

以上即是整個方法快速查找流程
整個流程圖流程圖如下

慢速流程@2x.png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,769評論 2 372