iOS底層 - objc_msgSend快速查找流程分析

iOS開發底層探究之路

上篇文章我們對cache原理進行了分析,在摸清cache是如何將方法信息存放進去后,我們來研究研究怎么取出所存儲的方法信息,那么本文將從objc_msgSend入手,探究如何快速獲取到方法信息。

編譯時 & 運行時(Runtime)

  • 編譯時:顧名思義就是正在編譯的時候,就是編譯器幫你把源代碼翻譯成機器能識別的代碼。(當然只是一般意義上這么說,實際上可能只是翻譯某個中間狀態語言)做一些詞法分析語法分析及幫你檢查代碼錯誤的過程。可理解為一個靜態過程。
  • 運行時:就是代碼在內存中跑起來的整個過程。因為編譯過的代碼只是保存在磁盤上,并沒有裝入內存中,而且運行時所做的一些檢查判斷工作與編譯時不一樣。可理解為一個動態過程。
編譯時&運行時及代碼結構圖

上圖可以看出,代碼層與Runtime底層庫之間有一個編譯層,在我們代碼運行時候,我們所進行的操作,比如調用方法,給屬性賦值等操作時,上層代碼與在經過編譯后是不一樣,就像之前碰到過的alloc方法,編譯器處理過就變為objc_alloc,及isKindOfClass方法變成了objc_opt_isKindOfClass
我們可以利用上層代碼使用到底層Runtime:

  • [person SomeMethod] :對象方法調用
  • isKindOfClassFrameworks&Service
  • class_getInstaceSizeRuntime API

例子探究方法本質

創建一個類LGPerson,添加兩個方法sayHellosayNB:

@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end

@implementation LGPerson
- (void)sayNB{
    NSLog(@"666");
}
@end

只實現sayNB方法,暫不實現sayHello方法。在main.m文件的 main方法中初始化person對象并調用sayHellosayNB方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       LGPerson *person = [LGPerson alloc];
       [person sayNB];
       [person sayHello];
       NSLog(@"Hello, World!");
    }
    return 0;
}

此時,只編譯項目的話,發現能編譯成功,接著運行的話,項目就會崩潰,并且報錯未在LGPerson類中找到sayHello方法實現。那么我們就能證明編譯時代碼不運行起來,也就不會檢查是否方法有實現了,運行時會去檢查是否方法已經實現了,也就是說在運行時找不到方法實現的話程序會崩潰
objc_msgSend初探
接著我們使用clang編譯main.cpp文件,通過查看main函數中方法調用的實現:

LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

可以看出,不管是類方法還是實例方法,方法調用的本質就是objc_msgSend方法的調用。為驗證這個,我們可以直接調用objc_msgSend方法,傳入正確參數即可:

LGPerson *person = [LGPerson alloc];
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];

1.引入頭文件#import <objc/message.h>
2.target --> Build Settingenable strict checking of obc_msgSend callsYES 改為NO,不然會報錯。

打印結果如下發現直接調用方法和利用objc_msgSend方法調用結果一致:

objc_msgSend與sayNB運行結果

方法isa走位再次探究
再添加一個LGPerson的父類LGTeacher類,并且讓LGTeacher類實現sayHello方法:

@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
    NSLog(@"666");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
    NSLog(@"666");
}
@end

此時編譯并運行程序,發現程序不會奔潰,并能正常打印輸出結果。我們可以猜測對象方法在此找不到就會去父類里面查找的猜想。
我們也可直接利用objc_msgSendSuper方法來調用父類方法:

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[person sayHello];

struct objc_super lgsuper;
 lgsuper.receiver = person;
 lgsuper.super_class = [LGTeacher class];
 objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));

發現直接person調用sayHello方法和利用objc_msgSendSuper方法,都能正常獲取到方法的實現,正常輸出打印結果。

objc_msgSend方法快速查找流程分析

objc4-781源碼可執行工程中全局搜索objc_msgSend,在匯編文件
objc_msg_arm64.s中找到objc_msgSend的入口ENTRY

    ENTRY _objc_msgSend
//--- 無窗口
    UNWIND _objc_msgSend, NoFrame
//--- 判斷當前p0和空對比,判斷是否為nil,p0為當前方法第一個參數即消息接受者receiver
    cmp p0, #0          // nil check and tagged pointer check
//---支持taggedpointer(小對象類型)
#if SUPPORT_TAGGED_POINTERS
//--- le 小于 進入 LNilOrTagged 流程
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
//--- eq 等于0,直接返回 空
    b.eq    LReturnZero
#endif
//--- p0即receiver 到這里說明存在
//--- 根據對象拿出isa, 即從寄存器x0指向的地址 取出isa,存入 p13 寄存器
    ldr p13, [x0]       // p13 = isa
//--- 在拿到isa后再去獲取當前class信息,利用 isa(p13) & ISA_MASK,拿出相應的isa中的shiftcls信息,即獲得calss信息
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
//--- 緩存查找,也就是所謂的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
  • 首先判斷當前消息接收者receiver是否存在,如果不存在就直接返回空
  • 判斷是否支持Tagged_Pointers,支持的話跳轉到LNilOrTagged
    • 如果等于0,則返回
    • 如果不為0,則對小對象的isa處理,并進入LGetIsaDone步驟
  • 如果不是小對象,且receiver存在,則獲取到當前對象的isa,然后通過GetClassFromIsa_p16 獲取到當前類信息
    arm64匯編根據isa獲取類信息

在獲取isa流程完畢后,接著就進入CacheLookup,進行查找過程:

.macro CacheLookup
LLookupStart$1:

    // p1 = SEL, p16 = isa
//--- #define CACHE            (2 * __SIZEOF_POINTER__) 即2 * 8 = 16
//--- p11 = mask|buckets -- 從x16(isa)中平移 16 字節,取出cache 存入p11寄存器 -- 因為isa(8字節)  superClass(8字節)  cache(16字節) (真機maskAndBuckets mask高16位 + buckets低48位)
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 將p11 & 0x0000ffffffffffff 獲取到buckets 信息 高位mask處 抹0
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
//--- p11(cache) 右移48位,前面48補0得到mask信息,然后p1(_cmd-sel)& 得到的mask信息,即獲取到sel-imp 的下標index 存入p12
//--- cache存入sel-imp時候,也是按哈希算法 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

//--- p10 buckets數組首地址,p12 下標(_cmd & mask)<< 4,相當于 下標*16,再加上buckets首地址,獲取到存放當前_cmd的bucket(不確定的,不要做對比確認的),存入 p12寄存器
//--- bucket(16) = sel(8) + imp(8),一個bucket占用的大小為16,首地址偏移 獲取 當前的bucket
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT 等于 3

//--- 從x12中獲取出當前bucket對應的sel 及 imp,分別存入 p17 和 p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
//--- 比較 sel 與 p1(傳入的sel)
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
//--- 如果當前的bucket里沒有存放過sel\imp信息,就退出循環,因為 經過 hash算法 sel & mask 得到的下標位置 沒有存放任何sel\imp信息的話,也就是說此位置沒被別的占用,說明就沒有緩存次方法sel\imp信息,退出循環
    CheckMiss $0            // miss if bucket->sel == 0
//--- 判斷p12(當前下標對應的bucket首地址)是否等于p10(buckets數組第一個bucket的首地址)
    cmp p12, p10        // wrap if bucket == buckets
//--- 如果相等,跳轉到第三步 3f 定位到最后一個bucket地址
    b.eq    3f
//--- 如果bucket != buckets
//--- 通過對此位置每次進行BUCKET_SIZE大小遞減,即向前查找的方式,每次將對應的sel和imp存入p17 和 p9
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
//--- 獲取到最新的bucket對應的sel和imp,然后回到 第一步,繼續與參數_cmd比較
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人為設置查找位置為最后一個bucket
//--- mask = capacity - 1。
//--- p11(mask)右移44位,獲取到最后一個bucket的位置
    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.
//--- 此時查找位置為最后一個bucket ,比較sel與p1(_cmd)
    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
//--- 比較此時bucket是否回到了第一個位置
//--- 判斷是否是第一個bucket位置,還沒找到,說明里面buckets里面根本就沒找到 跳到下面 3f JumpMiss
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
//--- 向前查找規則,找到每一個bucket,然后回到第一步,繼續進行sel 與 _cmd 對比
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
//--- 找不到,只能去方法列表查找了
    JumpMiss $0

.endmacro
  • 此過程首先根據isa信息,經過地址偏移16獲取到cache信息,因為isasuperClass分別為8字節大小,真機環境下獲取到的cache 信息中為mask|buckets即 前面cache源碼分析文章看到的_maskAndBuckets 數據。然后分別利用_maskAndBuckets數據分別經過獲取到 bucketsmask , 利用哈希算法 _cmd & mask 獲取下標。然后根據buckets首地址偏移 下標 * 16bucketsize = sel + imp = 16),p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) ,其中PTRSHIFT = 3(_cmd & mask) << (1+PTRSHIFT) 相當于下標 左移 4 ,即乘以 16,為 相對于buckets首地址的偏移量。所以最后加上buckets首地址的話,就獲得了當前_cmd 對應的bucket

  • 接下來將當前獲取到的bucket中的sel_cmd對比,如果相等,說明找到了,直接返回對應的imp,如果不想等,進入下一步2f

  • 首先判斷如果此時的bucket中的sel = 0,即不存在sel、imp 信息,則說明根本就沒有對此方法進行過緩存, 結束搜索流程

  • 如果當前bucketsel 不等于_cmd ,則說明此位置被占用了,所以找別的位置,先判斷當前bucket是否是buckets中的第一個bucket, 如果是的話,進入3f流程,將查找位置移到buckets最后一個,然后以BUCKET_SIZE(16)大小遞減,遵循向前查找的規則,遍歷所有bucket,每獲取到一個bucket,進行sel_cmd比較。

  • 如果當前bucket 不是第一個bucket的話,遵循向前查找的規則,比較每一個bucketsel_cmd,如果遍歷到第一個還是沒找到,那么繼續將查找位置移到最后一個bucket位置,繼續向前查找,最后第二次到第一個bucket時候,就說明所有的bucket都不符合,所以結束查找流程。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念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