objc_msgSend 消息轉(zhuǎn)發(fā)流程探究一

運(yùn)行時(shí)理解


通過這張圖片我們可以看到姆怪,我們平時(shí)調(diào)用 oc 方法其實(shí)本質(zhì)就是調(diào)用 runtimeapi杯道,就是發(fā)消息夹厌。那么我們平常的 oc 方法調(diào)用豹爹,在底層又是如何實(shí)現(xiàn)的呢?

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface CXPerson : NSObject
- (void)sayHello;
@end

@implementation CXPerson
- (void)sayHello {
    NSLog(@"CXPerson - sayHello");
}
@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CXPerson *person = [CXPerson alloc];
        [person sayHello];
    }
    return 0;
}

我們先建立一個(gè)工程矛纹,在 main.m 文件定義一個(gè) CXPerson 類臂聋,并調(diào)用 sayHello 方法。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        CXPerson *person = ((CXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CXPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    }
    return 0;
}

然后打開終端或南,cd 到 main.m 所在文件目錄孩等,然后輸入 clang -rewrite-objc main.m -o main.cpp 命令,最后會(huì)看到生成了一個(gè) main.cpp 文件采够。 然后找到 main 函數(shù)的實(shí)現(xiàn)代碼肄方。可以看到 allocsayHello 方法的調(diào)用在底層都會(huì)轉(zhuǎn)為 objc_msgSend 的調(diào)用形式蹬癌。

通過代碼可以看到消息的發(fā)送需要兩個(gè)參數(shù) objc_msgSend(消息接受者, 消息的主題(sel + 參數(shù)))权她,消息接收者跟消息主題。那么我們是否能直接調(diào)用底層的消息發(fā)送方法呢逝薪?這里我們來試一下隅要。


這里可以看到,我們自己直接調(diào)用 objc_msgSend 方法也是可以的董济。

objc_msgSend 源碼探究


這里我們來看一下 objc_msgSend 方法再源碼中的實(shí)現(xiàn)步清,全局搜索 objc_msgSend 我們可以看到,不同架構(gòu)下 objc_msgSend 對(duì)應(yīng)的匯編代碼虏肾。因?yàn)槲覀冋鏅C(jī)常用的是 arm64 架構(gòu)廓啊,這里我們就分析一下 arm64 架構(gòu)下的匯編實(shí)現(xiàn)。

  1. ENTRY _objc_msgSend (進(jìn)入到 objc_msgSend )
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

// p0代表消息接受者的地址询微,這里判斷消息接收者是否存在
    cmp p0, #0          // nil check and tagged pointer check

// 如果消息接受者不存在崖瞭,就會(huì)走到這里。這這里也會(huì)有判斷
#if SUPPORT_TAGGED_POINTERS
// 如果為SUPPORT_TAGGED_POINTERS類型走到這里
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
// 不為SUPPORT_TAGGED_POINTERS類型走到這里
    b.eq    LReturnZero

// 如果消息接收者存在就會(huì)走到這里
#endif
    ldr p13, [x0]       // p13 = isa (消息接收者的首地址)
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
// 這個(gè)通過消息接收者 receviece 獲取 class 的原因是因?yàn)榉椒ù嬖?cache 里面撑毛,而 cache 是類結(jié)構(gòu)體的屬性书聚。cache 中有 cache_getIMP方法。通過 sel 獲取 imp藻雌。
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

這一步主要是判斷消息接受者是否存在雌续,如果存在的話就把 isa 賦值給 p13,并作為參數(shù)帶入到 GetClassFromIsa_p16 方法胯杭。

  1. .macro GetClassFromIsa_p16 src, needs_auth, auth_address (通過 isa 獲取 class)
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, \src           // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    mov p16, \src
.else
    // 64-bit packed isa
        // and    $0, $1, #ISA_MASK $1就是 p13,就是 isa驯杜,與上ISA_MASK得到 class,并賦值給 p16
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov p16, \src

#endif

這一步主要是通過 isa 獲取 class做个,因?yàn)?cache 是類結(jié)構(gòu)體的一個(gè)屬性鸽心。而 chache_getIMP 存在于 cache 中滚局。


全局搜索可以看到 ExtractISA 的實(shí)現(xiàn),代表 $1 與上 ISA_MASK 并賦值給 $0顽频。

  1. .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant


// 把 `p16` 移動(dòng)到 `x15`
    mov x15, x16            // stash the original isa


// 開始進(jìn)入objc_msgSend流程
LLookupStart\Function:
    // p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    ldr p10, [x16, #CACHE]              // p10 = mask|buckets
    lsr p11, p10, #48           // p11 = mask
    and p10, p10, #0xffffffffffff   // p10 = buckets
    and w12, w1, w11            // x12 = _cmd & mask


#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 如果是 arm64 結(jié)果會(huì)走到這里
    ldr p11, [x16, #CACHE]          // p11 = mask|buckets


    #if CONFIG_USE_PREOPT_CACHES
    // 全局搜索CONFIG_USE_PREOPT_CACHES(#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST#define CONFIG_USE_PREOPT_CACHES 1) 得到CONFIG_USE_PREOPT_CACHES等于 1藤肢,所以走到這里
      #if __has_feature(ptrauth_calls)
    tbnz    p11, #0, LLookupPreopt\Function
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
      #else


    //p11` 與上 `0x0000fffffffffffe` 得到 `buckets` 并賦值給 `p10
    and p10, p11, #0x0000fffffffffffe   // p10 = buckets
    //p11跟 0 相比較,如果不為零就跳轉(zhuǎn)到到 LLookupPreopt
    tbnz    p11, #0, LLookupPreopt\Function

      #endif
// 如果為零會(huì)走到這里糯景。p1 右移7 個(gè)位置賦值給 x12
    eor p12, p1, p1, LSR #7
    and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else

// 如果模擬器會(huì)走到這里嘁圈,p11為 cache,與上0x0000ffffffffffff得到 buckets 并賦值給 p10
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
//p11右移動(dòng) 48 位就會(huì)得到 mask 的值
// p1(_cmd) & mask -> index -> p12 = index
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask

#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    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


// p11 cache -> p10 = buckets
// p1(_cmd) & mask = index -> p12
// 全局搜索 PTRSHIFT 等于 3 #define PTRSHIFT 3  
// (_cmd & mask) << (1+PTRSHIFT) ->  (_cmd & mask) << 4
// buckets + 內(nèi)存平移(1蟀淮,2最住,3,4) = b[i]
// p13 當(dāng)前查找的 bucket
    add p13, p10, p12, LSL #(1+PTRSHIFT)
                                                // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))


                        // do {
// * bucket-- p17, p9同時(shí)存入bucket--
// 拿到相應(yīng)的bucket里面的東西 imp(p17)  sel(p9)
// 查到的 sel 跟我們要查的 sel 進(jìn)行比較怠惶,如果一樣涨缚,就會(huì)走到 2 里面,不等于就會(huì)走到 3 里面策治,其實(shí)就是循環(huán)查找
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel != _cmd) {
    b.ne    3f              //         scan more
                        //     } else {
2:  CacheHit \Mode              // hit:    call or return imp
                        //     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
    b.hs    1b

調(diào)用 CacheLookup 方法并帶入 NORMAL, _objc_msgSend, __objc_msgSend_uncached 這幾個(gè)參數(shù)仗岖。

  1. ldr p11, [x16, #CACHE]
    x16 的地址平移 CACHE 大小。
  2. #define CACHE (2 * __SIZEOF_POINTER__)
    全局搜索 #define CACHE 可以得到 CACHE 是兩個(gè)指針的大小為 16個(gè)字節(jié)览妖。
  3. [x16, #CACHE] -> p11 -> cache_t
    所以 p11 就是等于 cache_t轧拄。
  4. CONFIG_USE_PREOPT_CACHES
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1

全局搜索 CONFIG_USE_PREOPT_CACHES,得到 CONFIG_USE_PREOPT_CACHES 等于 1讽膏。

//p11` 與上 `0x0000fffffffffffe` 得到 `buckets` 并賦值給 `p10
and p10, p11, #0x0000fffffffffffe   // p10 = buckets
//p11跟 0 相比較檩电,如果不為零就跳轉(zhuǎn)到到 LLookupPreopt
tbnz    p11, #0, LLookupPreopt\Function
  1. LLookupPreopt\Function

  2. CacheHit

.macro CacheHit
.if $0 == NORMAL
// 如果 $0 等于 NORMAL就會(huì)走到TailCallCachedImp
    TailCallCachedImp x17, x10, x1, x16 // authenticate and 
  1. TailCallCachedImp
//CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro TailCallCachedImp
    // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// $0(x17) ^  $3(isa) = 編碼(imp)
    eor $0, $0, $3
// 跳轉(zhuǎn)到$0(imp),就是執(zhí)行 imp 方法
    br  $0

最后總結(jié)

objc_msgSend(recevier, _cmd) sel -> imp

  1. 判斷 recevier 是否存在
  2. 通過 recevier 找到 isa 找到 class(p16)
  3. class 內(nèi)存平移得到 cache(bucket mask)
  4. bucket掩碼 -> bucket
  5. mask掩碼 -> mask
  6. insert哈希函數(shù)(mask_t)(value & mask);
  7. 第一次查找 index
  8. bucket + index -> 整個(gè)緩存里的第幾個(gè) bucket
  9. bucket{imp, sel}
  10. sel 與我們查找的 _cmd比較,如果相等 -> cacheHit(緩存命中) -> imp ^ class(isa) = imp -> (br)調(diào)用 imp
  11. 如果不相等 bucket-- ->再次平移 -> 找到 bucket 再次比較
  12. 死循環(huán) 遍歷查找
  13. 一直找不到就會(huì)走的 __objc_msgSend_uncached(執(zhí)行CacheLookup方法帶入的第三個(gè)參數(shù))

未完待續(xù)府树。俐末。。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奄侠,一起剝皮案震驚了整個(gè)濱河市卓箫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌垄潮,老刑警劉巖烹卒,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異弯洗,居然都是意外死亡旅急,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門牡整,熙熙樓的掌柜王于貴愁眉苦臉地迎上來藐吮,“玉大人,你說我怎么就攤上這事∫ゴ牵” “怎么了迫摔?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長泥从。 經(jīng)常有香客問我攒菠,道長,這世上最難降的妖魔是什么歉闰? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮卓起,結(jié)果婚禮上和敬,老公的妹妹穿的比我還像新娘。我一直安慰自己戏阅,他們只是感情好昼弟,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奕筐,像睡著了一般舱痘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上离赫,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天芭逝,我揣著相機(jī)與錄音,去河邊找鬼渊胸。 笑死旬盯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翎猛。 我是一名探鬼主播胖翰,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼切厘!你這毒婦竟也來了萨咳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤疫稿,失蹤者是張志新(化名)和其女友劉穎培他,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遗座,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡靶壮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了员萍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腾降。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖碎绎,靈堂內(nèi)的尸體忽然破棺而出螃壤,到底是詐尸還是另有隱情抗果,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布奸晴,位于F島的核電站冤馏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏寄啼。R本人自食惡果不足惜逮光,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望墩划。 院中可真熱鬧涕刚,春花似錦、人聲如沸乙帮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽察净。三九已至驾茴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氢卡,已是汗流浹背锈至。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留译秦,地道東北人裹赴。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像诀浪,于是被迫代替她去往敵國和親棋返。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容