objc_msgSend流程分析

一.什么是 Runtime乏矾?

  • 我們都知道,將源代碼轉(zhuǎn)換為可執(zhí)行的程序,通常要經(jīng)過三個(gè)步驟:編譯钻心、鏈接凄硼、運(yùn)行。不同的編譯語言捷沸,在這三個(gè)步驟中所進(jìn)行的操作又有些不同摊沉。
  • C 語言 作為一門靜態(tài)類語言,在編譯階段就已經(jīng)確定了所有變量的數(shù)據(jù)類型痒给,同時(shí)也確定好了要調(diào)用的函數(shù)说墨,以及函數(shù)的實(shí)現(xiàn)
  • Objective-C 語言 是一門動(dòng)態(tài)語言苍柏。在編譯階段不知道變量的具體數(shù)據(jù)類型尼斧,也不知道所真正調(diào)用的哪個(gè)函數(shù)。只有在運(yùn)行時(shí)間才檢查變量的數(shù)據(jù)類型试吁,同時(shí)在運(yùn)行時(shí)才會(huì)根據(jù)函數(shù)名查找要調(diào)用的具體函數(shù)棺棵。這樣在程序沒運(yùn)行的時(shí)候,我們并不知道調(diào)用一個(gè)方法具體會(huì)發(fā)生什么熄捍。
  • Objective-C 語言 把一些決定性的工作從編譯階段烛恤、鏈接階段推遲到 運(yùn)行時(shí)階段 的機(jī)制,使得 Objective-C 變得更加靈活余耽。我們甚至可以在程序運(yùn)行的時(shí)候缚柏,動(dòng)態(tài)的去修改一個(gè)方法的實(shí)現(xiàn),這也為大為流行的『熱更新』提供了可能性碟贾。
  • 而實(shí)現(xiàn) Objective-C 語言 運(yùn)行時(shí)機(jī)制 的一切基礎(chǔ)就是 Runtime币喧。
  • Runtime 實(shí)際上是一個(gè)庫,這個(gè)庫使我們可以在程序運(yùn)行時(shí)動(dòng)態(tài)的創(chuàng)建對(duì)象缕陕、檢查對(duì)象粱锐,修改類和對(duì)象的方法

Runtime交互的三種方式

  • Objective-C Code直接調(diào)用,比如直接調(diào)用方法[self say]扛邑、#selector()等怜浅。

  • Framework&Serivce,比如NSSelectorFromString蔬崩、isKindeofClass恶座、isMemberOfClass等方法。

  • RuntimeAPI沥阳,比如sel_registerName跨琳、class_getInstanceSize等底層方法。

  • runtime 特性動(dòng)態(tài)類型,動(dòng)態(tài)綁定,動(dòng)態(tài)加載

二.objc_msgSend探索

1.什么是objc_msgSend?官方文檔給出這么一個(gè)解釋

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

大概意思是:
遇到方法調(diào)用時(shí)桐罕,編譯器會(huì)生成對(duì)objc_msgSend脉让,objc_msgSend_stret桂敛,objc_msgSendSuperobjc_msgSendSuper_stret函數(shù)之一的調(diào)用。 發(fā)送到對(duì)象超類的消息(使用super關(guān)鍵字)是使用objc_msgSendSuper發(fā)送的溅潜; 其他消息使用objc_msgSend發(fā)送术唬。 使用objc_msgSendSuper_stretobjc_msgSend_stret`發(fā)送具有數(shù)據(jù)結(jié)構(gòu)作為返回值的方法。

2.準(zhǔn)備可運(yùn)行objc源碼工程

int main(int argc, const char * argv[]) {
    @autoreleasepool { 
        PHPerson * person = [[PHPerson alloc]init];
        Class cls = object_getClass(person);
        [person doFirst];
        [person doSecond];
        [person doThird];
        NSLog(@"");
    }
    return 0;
}
  • 新建一個(gè)PHPerson類滚澜,對(duì)象方法doFirst/doSecond/doThird
  • cd到當(dāng)前工程main.m文件所在到文件夾
  • 執(zhí)行 clang -rewrite-objc main.m 轉(zhuǎn)成.cpp文件
  • 找到.cpp文件
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        PHPerson * person = ((PHPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((PHPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("PHPerson"), sel_registerName("alloc")), sel_registerName("init"));
        Class cls = object_getClass(person);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doFirst"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doSecond"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doThird"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_4j_v597272j6kb0q7g5x72k3xhw0000gn_T_main_d18b1e_mi_0);
    }
    return 0;
}

每次調(diào)用方法的時(shí)候都會(huì)存在((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("方法名"));
3.斷點(diǎn)調(diào)試

  • 下斷點(diǎn)


  • 開啟匯編調(diào)試




結(jié)果:我們通過給objc_msgSend下符號(hào)斷點(diǎn)得知objc_msgSend函數(shù)在我們的libobjc.A.dylib中粗仓。

4.匯編分析(筆者匯編不大懂,查資料理解设捐,不正確的地方希望多多指正)

  • 關(guān)掉匯編分析借浊,下符號(hào)斷點(diǎn),并重新運(yùn)行工程萝招。


    image.png

    跳轉(zhuǎn)到_objc_msg-x86_64.s下 根據(jù)r10 = self->isa 可知此過程是獲取對(duì)象的isa指針


  • 接下來我們?cè)?code>libobjc.A.dylib中來查看我們的objc_msgSend源碼蚂斤。

  • 由于app真機(jī)環(huán)境下基于arm64架構(gòu),我們以objc-msg-arm64.s為研究對(duì)象即寒。

我們發(fā)現(xiàn)objc_msgSend使用匯編來實(shí)現(xiàn)的橡淆,為什么要用匯編來實(shí)現(xiàn)呢?有以下幾點(diǎn)原

  • 匯編更加容易被機(jī)器識(shí)別母赵,效率更高。
  • C語言中不可以通過一個(gè)函數(shù)來保留未知的參數(shù)并且跳轉(zhuǎn)到任意的函數(shù)指針具滴。C語言沒有滿足這些事情的必要特性凹嘲。
  • 截取部分代碼
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    NilTest NORMAL
    GetIsaFast NORMAL       // r10 = self->isa
    // calls IMP on success
    CacheLookup NORMAL, CALL, _objc_msgSend
    NilTestReturnZero NORMAL

    GetIsaSupport NORMAL

我們可以看到在獲取isa之后在方法的緩存列表繼續(xù)查找

  • 緩存查找

全局搜索CacheLookup

image.png

截取代碼

.macro CacheLookup
LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#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

    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
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    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
{imp, sel} = *bucket
    // 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

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

可以看到CacheHitCheckMiss兩個(gè)函數(shù),CacheHit是命中緩存(call or return imp构韵,方法返回的imp指針)周蹭,CheckMiss是沒有在緩存方法列表找到方法的函數(shù),此時(shí)主要分析CheckMiss這個(gè)函數(shù)內(nèi)部

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

由于現(xiàn)在的$0NORMAL疲恢,繼續(xù)查找__objc_msgSend_uncached凶朗,發(fā)現(xiàn)內(nèi)部都調(diào)用了MethodTableLookup函數(shù)返回

  • arm64架構(gòu)下MethodTableLookup函數(shù)部分源碼

.macro MethodTableLookup
    
....
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    bl  _lookUpImpOrForward
....
    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR
.endmacro

內(nèi)部調(diào)用了_lookUpImpOrForward函數(shù),看看這個(gè)函數(shù)內(nèi)部干了啥

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
}
  • 注釋中有一段比較重要段話翻譯如下

runtimeLockisrealizationisInitialized檢查期間被保持显拳,以防止與并發(fā)實(shí)現(xiàn)的競(jìng)爭(zhēng)棚愤。runtimeLock在方法搜索期間進(jìn)行,使方法查找+緩存填充相對(duì)于方法添加具有原子性杂数。
否則宛畦,可能會(huì)添加一個(gè)類別,但會(huì)無限期地忽略它揍移,因?yàn)樵?code>緩存刷新后會(huì)用舊值重新填充緩存次和。

if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
   }

如果sel == initialize,則class_initialize將發(fā)送+initialize那伐,然后信使將在此過程結(jié)束后再次發(fā)送+initialize踏施。當(dāng)然石蔗,如果這不是被信使調(diào)用,那么它不會(huì)發(fā)生畅形。

if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

遞歸超找父類方法緩存列表


可以看到在查找方法實(shí)現(xiàn)的時(shí)候有個(gè)cache_getImp函數(shù)养距,繼續(xù)跟進(jìn)

END_ENTRY _cache_getImp


/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward is the externally-callable
*   function returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
*   method caches.
*
********************************************************************/
    STATIC_ENTRY __objc_msgForward_impcache
    // No stret specialization.
    b   __objc_msgForward
    END_ENTRY __objc_msgForward_impcache

    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgForward
    cache_fill
    ENTRY _objc_msgSend_noarg
    b   _objc_msgSend
    END_ENTRY _objc_msgSend_noarg
realizeClassMaybeSwiftAndLeaveLocked
    ENTRY _objc_msgSend_debug
    b   _objc_msgSend
    END_ENTRY _objc_msgSend_debug

    ENTRY _objc_msgSendSuper2_debug
    b   _objc_msgSendSuper2
    END_ENTRY _objc_msgSendSuper2_debug

    ENTRY _method_invoke
    // x1 is method triplet instead of SEL
    add p16, p1, #METHOD_IMP
    ldr p17, [x16]
    ldr p1, [x1, #METHOD_NAME]
    TailCallMethodListImp x17, x16
    END_ENTRY _method_invoke

注釋的一段話大概意思是:_objc_msgForward是外部可調(diào)用的函數(shù),由method_getImplementation()等函數(shù)返回束亏。_objc_msgForward_impcache是實(shí)際存儲(chǔ)在方法緩存中的函數(shù)指針铃在。

method_getImplementation(Method m)
{
    return m ? m->imp : nil;
}

三.總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碍遍,隨后出現(xiàn)的幾起案子定铜,更是在濱河造成了極大的恐慌,老刑警劉巖怕敬,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揣炕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡东跪,警方通過查閱死者的電腦和手機(jī)畸陡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虽填,“玉大人丁恭,你說我怎么就攤上這事≌眨” “怎么了止喷?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵概疆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng)旷太,這世上最難降的妖魔是什么梁钾? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任比搭,我火速辦了婚禮范舀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衫樊。我一直安慰自己飒赃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布橡伞。 她就那樣靜靜地躺著盒揉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兑徘。 梳的紋絲不亂的頭發(fā)上刚盈,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音挂脑,去河邊找鬼藕漱。 笑死欲侮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肋联。 我是一名探鬼主播威蕉,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼橄仍!你這毒婦竟也來了韧涨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤侮繁,失蹤者是張志新(化名)和其女友劉穎虑粥,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宪哩,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡娩贷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锁孟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片彬祖。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖品抽,靈堂內(nèi)的尸體忽然破棺而出储笑,到底是詐尸還是另有隱情,我是刑警寧澤圆恤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布南蓬,位于F島的核電站,受9級(jí)特大地震影響哑了,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烧颖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一弱左、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炕淮,春花似錦拆火、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至润歉,卻和暖如春模狭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背踩衩。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工嚼鹉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贩汉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓锚赤,卻偏偏與公主長(zhǎng)得像匹舞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子线脚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361