iOS底層探索之objc_msgSend流程——快速查找

objc_msgsend

runtime運行時

  • 編譯時:顧名思義就是正在編譯的時候,把源代碼編譯成機器能識別的語言,主要是對語言進行最基本的檢查報錯,即詞法分析、語法分析等,是一個靜態的階段。command + b

  • 運行時:就是代碼跑起來被裝載到內存中去了,如果此時出錯,程序會崩潰,是一個動態的階段。command + R
    runtime:一套由C/C++``匯編寫成的為我們OC提供運行時功能的API。現行對應的編程接口:Objective2.0。runtime的使用有以下三種方式,其中三種實現方法與編譯層和底層的關系圖,如下圖所示:

    image.png

  • 通過OC代碼,如直接調用方法[LGPerson sayHello]等。

  • 通過NSObject方法,如isKindOfClassisMemberOfClass等。

  • 通過runtim API,如class_getInstanceSize等底層方法。
    其中Compiler就是我們了解的編譯器,即LLVMruntime System Library--底層庫。

探索OC方法本質

  • 1、新建一個Person類,并聲明一個實例方法。
@interface Person : NSObject
-(void)eat;
@end

@implementation Person
-(void)eat {
    NSLog(@"%s",__func__);
}
@end
  • 2、在main.h文件中,初始化Person,并調用eat方法。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person * p = [Person alloc];
        [p eat];
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 3、執行clang,將main.m文件編譯成main.cpp文件,在main.cpp文件中我們找到main函數。
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jf_j8m194qs4517fp_kg01ywhhh0000gn_T_main_b774b2_mi_0);
        Person * p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
    }
    return 0;
}

分析main.cpp文件的main函數我們發現,在調用alloceat方法時都使用了objc_msgSend這個方法,消息發送。然后通過runtime直接在main函數使用這個objc_msgSend方式調用一下eat方法,看能否打印出結果。

1、直接調用objc_msgSend,需要導入頭文件#import <objc/message.h>

2、需要將target --> Build Setting -->搜索msg -- 將enable strict checking of obc_msgSend calls由YES 改為NO,將嚴厲的檢查機制關掉,否則objc_msgSend的參數會報錯。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person * p = [Person alloc];
        [p eat];
        objc_msgSend((id)p, sel_registerName("eat"));
        NSLog(@"Hello, World!");
    }
    return 0;
}

運行打印結果如下:


image.png

調用父類方法
新建Teacher類繼承于Person類。

#import "Person.h"
@interface Teacher : Person
@end
@implementation Teacher
@end

在main.m文件中,使用聲明并teacher,并調用eat方法;使用objc_msgSendSuper調用eat方法。

struct objc_super teacher;
Teacher * t = [Teacher alloc];
[t eat];
teacher.receiver = t;
teacher.super_class = [t class];
objc_msgSendSuper(&teacher, sel_registerName("eat"));

打印結果如下:


image.png

發現:兩種方式都調用了父類Personeat的方法。
方法的本質就是發送消息,OC調用方法等價于runtime中的objc_msgSend和objc_msgSendSuper消息發送方法的調用,首先是從類中查找,如果類中沒有找到,就會去類的父類中查找。

objc_msgSend快速查找流程分析

objc4-781源碼中,搜索objc_msgSend,由于我們日常開發的都是架構是arm64,所以需要在arm64.s后綴的文件中查找objc_msgSend源碼實現,發現是匯編實現。

匯編的主要特性:速度快,更容易被機器識別。方法參數的動態性,匯編調用函數時傳遞的參數是不確定的,那么發送消息時,直接調用一個函數就可以發送所有的消息。

快速查找:cache中查找

慢速查找:methodList中查找,消息轉發

流程如下圖:


objc_msgSend流程分析

源碼分析:

//消息發送 --匯編入口--obj_msgSend主要是拿到接收者的isa信息
    ENTRY _objc_msgSend
//--無窗口
    UNWIND _objc_msgSend, NoFrame
//--比較p0和空做對比,判斷p0(recevier)消息接收者對象是否為空,其中p0是objec_msgSend的第一個參數-消息接收者receiver
    cmp p0, #0          // nil check and tagged pointer check
//--le小于--支持taggedpointer(小對象類型)的流程
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)如果支持了tagerpointer類型
#else
//--p0 等于0時 ,直接返回空
    b.eq    LReturnZero     //返回0
#endif
//--p0即receiver 肯定存在流程
//--根據對象拿出isa,即x0寄存器指向的地址 取出isa,存入 p13寄存器
    ldr p13, [x0]       // p13 = isa  消息存在,拿出isa-拿到類
//--在64位架構下通過p16 = isa (p13)& isa_mask ,拿出shiftcls信息,得到class信息
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
//--如果有isa,走到cachelookup,即緩存查找流程,也就是所謂的sel-imp快速查找流程
    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

1、首先判斷p0是否為空,objc_msgSend方法的第一個參數receiver是否為空
* 如果為空:直接返回0 LReturnZero
* 如果支持小對象tagged pointer,跳轉至LNilOrTagged
* 如果存在receiver(p0存在),不是小對象,從receiver中取出isa存入p13寄存器,
通過GetClassFromIsa_p16中,arm64架構下通過 isa & ISA_MASK獲取shiftcls位域的類信息,即classGetClassFromIsa_p16的匯編實現如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
//---- 將isa的值存入p16寄存器
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa--判斷是否是nonapointer isa
    // isa in p16 is indexed
//---- 將_objc_indexed_classes所在的頁的基址 讀入x10寄存器
    adrp    x10, _objc_indexed_classes@PAGE
//---- x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根據 偏移量 進行 內存偏移
    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
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//--用于64位系統
#elif __LP64__
    // 64-bit packed isa
//---- p16 = class = isa & ISA_MASK(位運算 & 即獲取isa中的shiftcls信息)
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
// 32-bit raw isa ---- 用于32位系統
    mov p16, $0

#endif

.endmacro

2、開啟緩存查找流程: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  #CACHE = 2 * pointer->平移16個字節得到cache_t
    //_maskAndBuckets   高16位為mask   bucket為低48位
//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 從x16(即isa)中平移16字節,取出cache 存入p11寄存器 -- isa距離cache 正好16字節:isa(8字節)-superClass(8字節)-cache(mask高16位 + buckets低48位)
    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  獲取buckets
//--p11(cache)右移48位,得到mask(即p11 存儲 mask),mask & p1(msgSend的第二個參數cmd-sel),得到sel-imp的下標index(即搜索下標)存入p12(cache insert寫入時的哈希下標計算是通過 sel & mask,讀取時也需要通過這種方式)
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask 邏輯右移 獲取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是下標,p10是buckets數組首地址,下標 * 1 << 4(即16)得到實際內存的偏移量,通過buckets的首地址偏移,獲取bucket存入p12寄存器
//--LSL#(1+PTRSHIFT)--實際含義就是得到一個bucket占用的內存的大小--相當于mask=occupied-1 --- _cmd & mask --取余數
    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(傳入的參數_cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)
//--如果不相等,即沒有找到,請跳轉至2f
    b.ne    2f          //     scan more
//如果相等 即CacheHit 緩存命中 直接返回imp
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
//--如果一直都找不到 因為是normal,跳轉值__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
//--判斷p12(下標對應的bucket) 是否等于 p0 (bucket是數組的第一個元素),如果等于則跳轉至第3步
    cmp p12, p10        // wrap if bucket == buckets
//--- 定位到最后一個元素(即第一個bucket)
    b.eq    3f
//--- 從x12(即p12 buckets首地址)- 實際需要平移的內存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//--- 跳轉至第1步,繼續對比 sel 與 cmd
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人為設置到最后一個元素
//--- p11(mask)右移44位 相當于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(傳入的參數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(下標對應的bucket) 是否 等于 p10(buckets數組第一個元素)-- 表示前面已經沒有了,但是還是沒有找到
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f   //如果等于,跳轉至第3步
//--- 從x12(即p12 buckets首地址)- 實際需要平移的內存大小BUCKET_SIZE,得到得到第二個bucket元素,imp-sel分別存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//--- 跳轉至第1步,繼續對比 sel 與 cmd
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
//--- 跳轉至JumpMiss 因為是normal ,跳轉至__objc_msgSend_uncached
    JumpMiss $0

.endmacro

匯編流程分析:
LLookupStart$1:-->很好解釋,就是開始查找
ldr p11, [x16, #CACHE]-->從x16(即isa)中平移16字節,取出cache 存入p11寄存器 -- isa距離cache正好16字節:isa(8字節)-superClass(8字節)-cache(mask高16位 + buckets低48位)

and p10, p11, #0x0000ffffffffffff-->p11(cache) & 0x0000ffffffffffff,mask16抹零,得到buckets存入p10寄存器--即去掉mask,留下buckets

and p12, p1, p11, LSR #48-->p11(cache)右移48位,得到mask(即p11存儲 mask),mask & p1msgSend的第二個參數cmd-sel),得到sel-imp下標index(即搜索下標)存入p12cache insert寫入時的哈希下標計算是通過sel & mask,讀取時也需要通過這種方式)

add p12, p10, p12, LSL #(1+PTRSHIFT)-->p10上面說了是bucket的首位,將其左移p12*4位(p12是傳進來方法,通過哈希算法得到的下標,每個bucket包含sel以及imp,所以每個bucket是16字節左移4位,就是左移16字節,下標乘于16,就拿到cache_t這個下標的bucket。)

1:方法
ldp p17, p9, [x12]-->通過bucket的結構體得到{imp, sel} = *bucket
cmp p9, p1-->是取的selp1傳進來的_cmd(就是傳進來的sel)不相等
b.ne 2f-->如果不相等,進入2
CacheHit $0-->如果相等,返回imp

2:方法:上面查找不相等,下面的方法是遞歸查找
CheckMiss $0-->如果從最后一個元素遍歷過來都找到不到,就返回CheckMiss
cmp p12, p10-->我們知道p10是第一個bucketp12是算的下標,這意思(判斷下標發現不是第一個)
b.eq 3f-->如果下標是第一個,走3
ldp p17, p9, [x12, #-BUCKET_SIZE]!-->如果不是第一個,就向前取bucket,循環一次對內存偏移-1,把取的bucket給p17
b 1b-->執行1

3:方法:上面發現是第一個bucket
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))-->p11右移48-(1+3)=44位,再跟第一次通過哈希算法的得到的下標p12,再次進行哈希算法。這次得到的這個下標是cache_t最后一位。

再查找一遍緩存后面再執行1,2方法
如果當前的bucket還是等于 buckets的第一個元素,則直接跳轉至JumpMiss,此時的$0normal,也是直接跳轉至__objc_msgSend_uncached,即進入慢速查找流程

CheckMiss和JumpMiss的匯編流程

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