OC消息轉(zhuǎn)發(fā)(一)— objc_msgSend探索

前言

該系列我們來探究一下OC的消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制旺韭,本文我們就來對(duì)objc_msgSend做一下初步探索,明白方法調(diào)用是如何快速尋找到方法的污筷。以后我們會(huì)探索到慢速尋找方法以及找不到方法是如何進(jìn)行消息轉(zhuǎn)發(fā)的。

runtime簡(jiǎn)介

要探索objc_msgSend,我們首先要了解runtime档泽。runtimeC袍啡、C++胖秒、匯編混合寫成的一套為Objective-C提供運(yùn)行時(shí)功能的API。也是因?yàn)?code>runtime梨撞,Object-C才被成為動(dòng)態(tài)語言燥滑。

runtime的版本

runtime的版本分為兩個(gè)版本modernlegacy官方文檔)渐北,我們現(xiàn)在使用的Objective-C 2.0版本就是modern版本,只能適用于iOS64 bit OS X 10.5版本及更高版本铭拧;legacy則適用于其他版本和32 bit OS X赃蛛。modernlegacy最大的區(qū)別就是如果更改類中實(shí)例變量的布局,legacy需要重新編譯他的子類搀菩,modern版本則不需要呕臂。

runtime的使用

runtime的使用大致可分為三種使用方法。

  • Objective-Ccode:@selector()等;
  • NSObject的方法:NSSelectorFromString()等;
  • runtimeapisel_registerName()等肪跋;
編譯時(shí)和運(yùn)行時(shí)

編譯時(shí):即編譯器對(duì)語言的編譯階段歧蒋,編譯時(shí)只是對(duì)語言進(jìn)行最基本的檢查報(bào)錯(cuò),包括詞法分析、語法分析等等疏尿,將程序代碼翻譯成計(jì)算機(jī)能夠識(shí)別的語言(例如匯編等)瘟芝,編譯通過并不意味著程序就可以成功運(yùn)行。
運(yùn)行時(shí):即程序通過了編譯這一關(guān)之后編譯好的代碼被裝載到內(nèi)存中跑起來的階段褥琐,這個(gè)時(shí)候會(huì)具體對(duì)類型進(jìn)行檢查锌俱,而不僅僅是對(duì)代碼的簡(jiǎn)單掃描分析,此時(shí)若出錯(cuò)程序會(huì)崩潰敌呈。這個(gè)階段也是runtime起作用的階段贸宏。

objc_msgSend探索

一、clang生成cpp文件
創(chuàng)建工程磕洪,在main.m寫入以下代碼:

void run(){
    NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //創(chuàng)建LGPerson類和對(duì)象方法sayNB
        LGPerson *person = [LGPerson alloc];
        [person sayNB];
        run();
    }
    return 0;
}

打開終端進(jìn)入main.m文件目錄下吭练,執(zhí)行以下命令:

clang -rewrite-objc main.m -o main.cpp

在此文件夾下會(huì)生成一個(gè)main.cpp文件,打開文件滑動(dòng)到底部可以看到如下代碼:

void run(){
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_85_h8yymn657hq3vfgnz_xwbtjc0000gp_T_main_26fe1b_mi_0,__func__);
}
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        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"));

        run();
    }
    return 0;
}

從代碼中可以看出析显,調(diào)用allocsayNB兩個(gè)方法被轉(zhuǎn)換成了objc_msgSend發(fā)送消息((void (*)(id, SEL))(void *)是類型強(qiáng)轉(zhuǎn))鲫咽,而我寫的一個(gè)run()函數(shù)則是直接調(diào)用,不是通過objc_msgSend進(jìn)行消息發(fā)送谷异,由此可以看出只有Objective-C的方法是通過runtime轉(zhuǎn)換為消息發(fā)送的分尸。

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

objc_msgSend的兩個(gè)參數(shù)idsel代表消息接收者和方法唯一標(biāo)識(shí)。
二歹嘹、斷點(diǎn)看匯編
sayNB處打斷點(diǎn)箩绍,如圖:

斷點(diǎn)sayNB

進(jìn)入斷點(diǎn),然后菜單 Debug -> Debug Workflow -> Always Show Disassembly尺上,顯示匯編如下:

objc_msgSend匯編

可以看到objc_msgSend,然后按著control+進(jìn)入objc_msgSend詳情怎抛,如下:

objc_msgSend詳情

可已看出objc_msgSend是在libobjc里邊,接下來我們?nèi)フ以创a看看objc_msgSend是如何快速進(jìn)行方法查找的唉窃。
三耙饰、objc_msgSend匯編源碼
objc_msgSend源碼是用匯編寫的,全局搜索objc_msgSend找到匯編(文件表示上為sarm64文件纹份,ENTRY _objc_msgSend是開始如下:

尋找objc_msgSend

objc_msgSend匯編源碼如下:

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

可以看出先進(jìn)行了niltagged pointer的檢測(cè)苟跪,SUPPORT_TAGGED_POINTERSarm64下為1廷痘,ldr p13, [x0]把在[x0]位置的isa存入p13中,GetClassFromIsa_p16 p13通過isa獲取class,GetClassFromIsa_p16詳情如下:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // 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__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

isa指針詳解文章中SUPPORT_INDEXED_ISA在iOS設(shè)備上是0件已,那么進(jìn)入and p16, $0, #ISA_MASK中笋额,也即是通過掩碼ISA_MASKisa獲取類信息。
接下來全局搜索CacheLookup篷扩,找到帶有.macro的宏定義兄猩,是CacheLookup詳情。如下:

.macro CacheLookup
    // p1 = SEL, p16 = class
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    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
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    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
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

參考類的結(jié)構(gòu)分析鉴未,分析上方匯編代碼:

注意:p枢冤、wx的區(qū)別
p16代表指針铜秆;w16代表32位下的值淹真,4字節(jié);x16代表64位下的值连茧,8字節(jié)核蘸;

  • ldp p10, p11, [x16, #CACHE]:全局搜索define #CACHE會(huì)發(fā)現(xiàn)#CACHE是16,通過GetClassFromIsa_p16可以知道x16代表class啸驯,對(duì)象結(jié)構(gòu)里內(nèi)存平移16位(isasuperclass)可以得到cache客扎,cache又包含了_buckets_mask_occupied坯汤。這句匯編的意思就是把_buckets存入到p10,把_mask_occupied存入到p11搀愧,又因?yàn)槭切《四J剑?code>p11 = occupied|mask惰聂。

  • and w12, w1, w11:這里用w是因?yàn)榇?字節(jié)只取4字節(jié),即w11=mask咱筛、w1sel轉(zhuǎn)換之后的key搓幌,w12存儲(chǔ)的是key&mask即方法在哈希表的索引值。

  • add p12, p10, p12, LSL #(1+PTRSHIFT)p10buckets的首地址迅箩,而bucket_t結(jié)構(gòu)體占用16字節(jié)溉愁,所以buckets的首地址加上索引向左偏移(1+PTRSHIFT)字節(jié)得到的值就是函數(shù)方法在緩存中的地址。因此p12就是函數(shù)方法對(duì)應(yīng)的bucket地址饲趋。

  • ldp p17, p9, [x12]:將bucket存放在p17p9中拐揭,p17impp9里裝sel奕塑。

  • 1: cmp p9, p1:比較取出來的selp1是否相等堂污,b.ne 2f不相等進(jìn)入2:CheckMiss $0緩存未命中;相等則是CacheHit $0緩存命中龄砰。CacheHit詳情如下:

// CacheHit: x17 = cached IMP, x12 = address of cached IMP
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    AuthAndResignAsIMP x0, x12  // authenticate imp and re-sign as IMP
    ret             // return IMP
.elseif $0 == LOOKUP
    AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

CacheHit就是找到了imp盟猖,那么直接調(diào)用TailCallCachedImp就完成了查找讨衣。

  • cmp p12, p10:比較p12p10是否相等,相等的話說明進(jìn)入3f:add p12, p12, w11, UXTW #(1+PTRSHIFT)式镐,索引值即為mask反镇;不相等則重新賦值p9,循環(huán)進(jìn)入1f娘汞。下方de就是進(jìn)入到循環(huán)查找imp的循環(huán)中了歹茶。
  • JumpMiss $0:跳轉(zhuǎn)到JumpMiss。如下:
.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

進(jìn)入NORMAL判斷中价说,調(diào)用__objc_msgSend_uncached辆亏。如下:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

然后調(diào)用MethodTableLookup,如下:

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

最后是調(diào)用了__class_lookupMethodAndLoadCache3方法鳖目,bl是跳轉(zhuǎn)方法扮叨,該方法還帶有雙下劃線,并且搜不到方法的具體實(shí)現(xiàn)领迈,可以得出該方法不再是匯編方法彻磁,應(yīng)該是跳轉(zhuǎn)到了C或者C++的方法。
到此我們就把objc_msgSend匯編快速查找方法的探索完了狸捅,那為什么要用匯編語言查找方法呢衷蜓?大概是有兩個(gè)原因:
1、這個(gè)過程需要的是速度尘喝,匯編更容易被計(jì)算機(jī)識(shí)別磁浇,速度更快。
2朽褪、因?yàn)榉椒ǘ紩?huì)有傳參和返回參數(shù)置吓,而且是不確定的,相對(duì)于C或者C++是很難實(shí)現(xiàn)這些的缔赠,但是匯編是可以的衍锚。

總結(jié)

1、Objective-C調(diào)用方法是一個(gè)通過objc_msgSend發(fā)送消息進(jìn)行查找方法的實(shí)現(xiàn)imp的嗤堰。
2戴质、objc_msgSend查找方法首先是匯編語言查找,這是一個(gè)快速的過程踢匣。還有一個(gè)是慢速查找的過程告匠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市离唬,隨后出現(xiàn)的幾起案子凫海,更是在濱河造成了極大的恐慌,老刑警劉巖男娄,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件行贪,死亡現(xiàn)場(chǎng)離奇詭異漾稀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)建瘫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門崭捍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人啰脚,你說我怎么就攤上這事殷蛇。” “怎么了橄浓?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵粒梦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我荸实,道長(zhǎ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
  • 文/蒼蘭香墨 我猛地睜開眼软驰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(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ú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有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
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至屠升,卻和暖如春潮改,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腹暖。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工汇在, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人脏答。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓糕殉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親殖告。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阿蝶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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

  • 消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過 selector 快速查找 ...
    lylaut閱讀 1,824評(píng)論 2 3
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí),它使得 Objective-C 如虎添翼黄绩,具備了靈活的...
    lylaut閱讀 795評(píng)論 0 4
  • 關(guān)于OC中的消息發(fā)送的實(shí)現(xiàn)羡洁,在去年也看過一次,當(dāng)時(shí)有點(diǎn)不太理解爽丹,但是今年再看卻很容易理解筑煮。 我想這跟知識(shí)體系的構(gòu)建...
    咖啡綠茶1991閱讀 938評(píng)論 0 1
  • 一辛蚊、clang指令探查方法調(diào)用 Clang是一個(gè)由Apple主導(dǎo)編寫,基于LLVM的C/C++/Objective...
    風(fēng)緊扯呼閱讀 889評(píng)論 0 3
  • 一年一度的購(gòu)物大戰(zhàn),打響了袒餐,你是不是希望搶到你想要的那個(gè)它飞蛹,從光的棍光棍節(jié)到今日全球狂歡的剁手節(jié)。起源于2009年...
    臻心向往閱讀 224評(píng)論 0 0