iOS中objc_msgSend流程學(xué)習(xí)

一纹因,前言

在iOS開發(fā)過程中,我們都知道不管是什么方法的執(zhí)行圾结,對(duì)象的創(chuàng)建鸭轮,以及代理Block的實(shí)現(xiàn)都離不開runtime,所以runtime可以說是iOS開發(fā)過程中的生命存在歪沃, 運(yùn)行時(shí) 存在 動(dòng)態(tài)決議 的作用,例如我們?cè)谝粋€(gè)類的聲明中聲明了相關(guān)的方法嫌松,但是并沒有進(jìn)行實(shí)現(xiàn)時(shí)沪曙,進(jìn)行編譯是不會(huì)有任何問題的,但是運(yùn)行時(shí)就會(huì)報(bào)錯(cuò)豆瘫,告知我們沒有實(shí)現(xiàn)該方法。接下來我們就重點(diǎn)研究一下運(yùn)行時(shí)為什么會(huì)只能的告訴我們沒有實(shí)現(xiàn)該方法菊值。

二外驱,環(huán)境配置

首先我們?cè)趍ain.m 中聲明一個(gè)類LGTeacher集成自NSObject,聲明一個(gè)方法sayHello,再次聲明一個(gè)類LGPerson繼承自LGTeacher腻窒,在LGPerson中重寫了父類的sayHello昵宇,以及從新聲明了sayNB,
代碼如下

@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

此時(shí)我們?cè)趍ain.m 中進(jìn)行創(chuàng)建相應(yīng)的對(duì)象儿子;并調(diào)用相應(yīng)的方法:

       LGPerson *person = [LGPerson alloc];
        
        LGTeacher *teacher = [LGTeacher alloc];
        
        [teacher sayHello];
        
        [person sayNB];

此時(shí)我們對(duì)該文件進(jìn)行相應(yīng)的clang命令瓦哎,生成應(yīng)的.cpp文件,對(duì)該文件進(jìn)行查看柔逼,我們能發(fā)現(xiàn)在運(yùn)行時(shí)runtime對(duì)我們的所有方法進(jìn)行相關(guān)的處理蒋譬;這就是runtime的作用;
以上的代碼在運(yùn)行時(shí)編譯成

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

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));

也就是說我們?cè)趧?chuàng)建對(duì)象的時(shí)候存在兩種對(duì)應(yīng)的關(guān)系愉适;

  • alloc 對(duì)應(yīng)的是 sel_registerName("alloc")
  • 方法調(diào)用 對(duì)應(yīng)是 sel_registerName("方法名"));

我們?cè)诳刂婆_(tái)打印的結(jié)果是

2020-09-20 23:01:12.225822+0800 001-運(yùn)行時(shí)感受[13893:304936] 666
2020-09-20 23:01:12.226333+0800 001-運(yùn)行時(shí)感受[13893:304936] 666

然后我們?cè)谟胷untime的形式進(jìn)行創(chuàng)建方法犯助,改寫原來的方法調(diào)用過程;

       [teacher sayHello];
        
        objc_msgSend(teacher, sel_registerName("sayHello"));
        
        [person sayNB];
        
        objc_msgSend(person, sel_registerName("sayNB"));

我們能看到這兩組打印結(jié)果完全是一樣的
所以我們得出的結(jié)論是
[teacher sayHello]objc_msgSend(teacher, sel_registerName("sayHello")); 完全等價(jià)维咸;那么為什么會(huì)是這樣的剂买,這就是我們接下來重點(diǎn)研究的對(duì)象objc_msgSend的查找流程

三,查找流程

objc_msgSend的方法查找過程中存在兩種查找流程癌蓖,一種是帶緩存的瞬哼,一種是不帶緩存,也即是快速和慢速的兩種情況租副,因?yàn)槲覀兩弦还?jié)學(xué)習(xí)了一個(gè)類的中的相關(guān)的cache_t的部分坐慰,并做了相關(guān)的詳細(xì)的介紹;所以里邊涉及了很多方法的存儲(chǔ)和對(duì)應(yīng)的imp的存儲(chǔ)過程用僧。那么接下來我們分別對(duì)兩個(gè)查找流程進(jìn)行一個(gè)學(xué)習(xí)和分析讨越。

1、快速查找

我們知道不管我們是何種語言實(shí)現(xiàn)的代碼永毅,在底層都會(huì)編譯為計(jì)算機(jī)能識(shí)別的語言把跨,也就是二進(jìn)制的代碼;那么計(jì)算機(jī)為什么能將我們寫的代碼轉(zhuǎn)換為二進(jìn)制語言了沼死,這就是計(jì)算機(jī)最高效的一種語言匯編着逐,這就是計(jì)算機(jī)能快速識(shí)別我們代碼的根本原因,那么我們就對(duì)匯編查找流程進(jìn)行一個(gè)分析和學(xué)習(xí)吧;

首先引入一個(gè)相關(guān)匯編指令介紹


匯編部分指令.png

我們打開開源代碼0bjc781,進(jìn)行編譯過后進(jìn)入相關(guān)的匯編代碼耸别;進(jìn)入?yún)R編文件

    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:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

匯編解析:

  • 1 ENTRY _objc_msgSend 進(jìn)入?yún)R編進(jìn)行方法查找入口健芭;
  • 2 NoFrame 進(jìn)入無窗口調(diào)試模式;
  • 3 cmp p0, #0 進(jìn)行判斷類是否是空秀姐;如果是空這進(jìn)入LReturnZero,否則進(jìn)入 LNilOrTagged
  • 4 ldr p13, [x0][x0] 中的信息讀取到p13 即將isa賦值給 p13, p13= 該類的isa
  • 5 獲取isa 進(jìn)行關(guān)聯(lián)類對(duì)象慈迈,相當(dāng)于alloc流程中的initWithIsa
  • 6 CacheLookup NORMAL, _objc_msgSend 獲取完isa 后,進(jìn)行正常的消息轉(zhuǎn)發(fā)過程省有;

CacheLookup的定義是什么痒留? 所以帶著刨根問底的理念全局搜索這個(gè)關(guān)鍵字,

CacheLookup 的查找有三種格式

  • 1 NORMAL
  • 2 GETIMP
  • 3 LOOKUP
    接下來我們著重分析一下 NORMAL 其他兩種情況剩余時(shí)間再去學(xué)習(xí)總結(jié)蠢沿,也算是一個(gè)學(xué)習(xí)的過程
    我們找到相關(guān)的CacheLookup定義 伸头;代碼如下

.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)

.endmacro

匯編分析

  • 1 ldr p11, [x16, #CACHE] 我們找到#CACHE 的定義是

#define CACHE (2 * __SIZEOF_POINTER__)

__SIZEOF_POINTER__ 我們都知道是8,所以 #CACHE 是16;
根據(jù)匯編語句也就是 ldr p11, [x16, 16] 也就是 x16 平移16位也就是順著ISA平移16位到Cache_t的位置舷蟀,也就是正如備注說的 p11 = mask|buckets 也就相當(dāng)于上次文章中介紹的找到相關(guān)的buckets的索引位置恤磷;index

的內(nèi)部結(jié)構(gòu)

  • 2 p10, p11, #0x0000ffffffffffff 通過掩碼找出取出緩存中的buckets 也就是 p11 = mask|buckets;
  • 3 and p12, p1, p11, LSR #48 因?yàn)?p11, LSR #48就相當(dāng)于
 static constexpr uintptr_t maskShift = 48;
    
    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;
    
    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) -

取出mask 在和p12 進(jìn)行按位與操作 也就是x12 = _cmd & mask

  • 4 add p12, p10, p12, LSL #(1+PTRSHIFT) 因?yàn)?p12, LSL #(1+PTRSHIFT) 我們找到PTRSHIFT 的定義如下

#define PTRSHIFT 3
也就是p12, LSL #4

也就是 p12左移4位也就是 左移16; p12 = *(buckets + index) *16 也就是找到相應(yīng)位置的bucket;

  • 5 ldp p17, p9, [x12] // {imp, sel} = *bucket取出第四步驟中取出的buckect中的selimp

  • 6 拿到相應(yīng)的Sel 和IMP 和我們調(diào)用的方法進(jìn)行對(duì)比野宜,如果沒有找到則跳轉(zhuǎn)到2 找到那么就進(jìn)入CacheHit定義

.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro
  • 7 再次沒查找到的時(shí)候向前查找所有的buckets扫步,如果都沒找到,則進(jìn)入 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

這就是整個(gè)快速查找過程匈子,

2锌妻、慢速查找

在此過程中我們知道調(diào)用方法就是獲取某個(gè)方法的IMP.所以我們找到項(xiàng)目的 class_getMethodImplementation 方法,在詳細(xì)研究相關(guān)的流程情況旬牲,代碼定義如下

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

再次尋根究底的探索相關(guān)的獲取imp的重點(diǎn)方法lookUpImpOrNil;再次進(jìn)入

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

再次順騰摸瓜進(jìn)入 lookUpImpOrForward

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    runtimeLock.assertUnlocked();
    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    runtimeLock.lock();

    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
     
    }

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

    runtimeLock.assertLocked();
    curClass = cls;

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

代碼解析過程仿粹;

  • 1 首先從緩存中取出相應(yīng)的imp,我們知道快速查找流程就是沒找到相應(yīng)的緩存;所以此處不可能存在imp.所以直接將IMP設(shè)置為nil;
  • 2 再次檢查是否是我們當(dāng)前的類對(duì)象原茅;
  • 3 如果當(dāng)前類是否是運(yùn)行相應(yīng)的查找行為權(quán)限吭历,如果有就繼續(xù)查找;
  • 4 如果當(dāng)前類中沒找到相應(yīng)的方法擂橘,繼續(xù)從父類方法列表中去查找晌区,直到找到NSObject 為止,
  • 5 如果找到通贞;那么就將該方法列表緩存起來朗若,為了下次能快速的查找,
  • 6 如果慢速都沒找到昌罩,直到返回nil的時(shí)候哭懈,那么就要進(jìn)行動(dòng)態(tài)方法解析。

這就是所有慢速查找流程的核心茎用,通過上述流程就能體現(xiàn)objec_msgSend的流程執(zhí)行遣总。

四睬罗,總結(jié);

慢速查找的原理就是在c/c++層面去進(jìn)行旭斥,只是做的事情是找到就進(jìn)行緩存操作容达,也是反復(fù)的遞歸找出我們所需的方法實(shí)現(xiàn)imp ,如果沒找到包括快速和慢速都沒有,那么后期將會(huì)繼續(xù)判斷是否允許動(dòng)態(tài)解析垂券,方法決議等花盐,如果不允許,則程序就會(huì)報(bào)錯(cuò)菇爪,告知我們這個(gè)方法沒有實(shí)現(xiàn)算芯,這就是這個(gè)objc_msgSend的流程,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末娄帖,一起剝皮案震驚了整個(gè)濱河市也祠,隨后出現(xiàn)的幾起案子昙楚,更是在濱河造成了極大的恐慌近速,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堪旧,死亡現(xiàn)場(chǎng)離奇詭異削葱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)淳梦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門析砸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人爆袍,你說我怎么就攤上這事首繁。” “怎么了陨囊?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵弦疮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蜘醋,道長(zhǎng)胁塞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任压语,我火速辦了婚禮啸罢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胎食。我一直安慰自己扰才,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布厕怜。 她就那樣靜靜地躺著训桶,像睡著了一般累驮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舵揭,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天谤专,我揣著相機(jī)與錄音,去河邊找鬼午绳。 笑死置侍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拦焚。 我是一名探鬼主播蜡坊,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赎败!你這毒婦竟也來了秕衙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤僵刮,失蹤者是張志新(化名)和其女友劉穎据忘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搞糕,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡勇吊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窍仰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汉规。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖驹吮,靈堂內(nèi)的尸體忽然破棺而出针史,到底是詐尸還是另有隱情,我是刑警寧澤碟狞,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布啄枕,位于F島的核電站,受9級(jí)特大地震影響篷就,放射性物質(zhì)發(fā)生泄漏射亏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一竭业、第九天 我趴在偏房一處隱蔽的房頂上張望智润。 院中可真熱鬧,春花似錦未辆、人聲如沸窟绷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兼蜈。三九已至攘残,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間为狸,已是汗流浹背歼郭。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辐棒,地道東北人病曾。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像漾根,于是被迫代替她去往敵國(guó)和親泰涂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345