IOS底層原理之objc_msgSend

一、clang指令探查方法調(diào)用

Clang是一個由Apple主導(dǎo)編寫,基于LLVM的C/C++/Objective-C編譯器。如果你不知道clang,可以在這里找到你想要的。

在工程目錄中的main.m文件目錄下進(jìn)入到終端,輸入如下命令

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

該命令會將main.m編譯成C++的代碼,但是不同平臺支持的代碼肯定是不一樣的。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的CPP文件
如果需要鏈接其他框架,使用-framework參數(shù)。比如-framework UIKit

在終端輸入命令以后,會生成一個main.cpp文件。打開main.cpp文件,直接將代碼拉到最下面,我們會看到這樣的一段代碼。

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

可以看到在OC層面調(diào)用的sayHello方法于底層而言是調(diào)用了一個objc_msgSend方法,那么我們可以確認(rèn)的是方法的調(diào)用其實是調(diào)用的objc_msgSend。

二、objc_msgSend底層實現(xiàn)

蘋果公司開源了objc_msgSend的底層![代碼]{https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/},是用匯編語言編寫的,其目的就是為了提高函數(shù)的執(zhí)行速度。蘋果公司提供諸多平臺架構(gòu)的匯編代碼,我這里是針對arm64平臺的匯編代碼(objc-msg-arm64.s)進(jìn)行分析。

1. 函數(shù)入口

全局搜索ENTRY _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

cmp是一個判斷指令。在這里判斷p0是否是0,如果為0則表示傳入的對象為nil,立即返回。
實際上SUPPORT_TAGGED_POINTERS的值定義為1,其定義在arm64-asm.h里面。b.le指令用來判斷上面的cmp的值是否小于等于執(zhí)行標(biāo)號,否則直接往下走。如果p0<0,則表示傳入的對象是tagged pointer。在這里我們不去討論tagged pointer的情況。程序繼續(xù)往下走,執(zhí)行如下代碼

ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

在這里將x0指向內(nèi)存地址的值isa賦值給p13,然后通過GetClassFromIsa_p16拿到class的地址。接下來CacheLookup流程,從緩存中查詢。

2、CacheLookup

來到CacheLookup流程,已經(jīng)將class的地址賦值給了p16。

ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#define SUPERCLASS       __SIZEOF_POINTER__
#define CACHE            (2 * __SIZEOF_POINTER__)

這里將class地址的偏移CACHE得到的地址給到p10和p11。superclass占用8個字節(jié),所以這里的偏移量是16字節(jié)。而類的底層定義是一個結(jié)構(gòu)體:

struct objc_class : objc_object {
    Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
};

isa占用8字節(jié),superclass占用8字節(jié),所以類地址偏移16字節(jié)可以得到cache。而對于cache_t結(jié)構(gòu)體的定義如下:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ...
};

buckets是結(jié)構(gòu)體指針,占用8字節(jié),mask占4字節(jié),occupied占4字節(jié),因此p16偏移16字節(jié)后得到buckets存儲在p10,p11存了mask和occupied,其中低32位表示mask,高32位表示occupied。

and w12, w1, w11        // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)  // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

p1是SEL,其低32位w1表示的時候SEL對應(yīng)的key,將key和mask相與得到函數(shù)方法在buckets哈希表中的索引。p10是buckets的首地址,而bucket_t結(jié)構(gòu)體占用16字節(jié),所以buckets的首地址加上索引向左偏移4字節(jié)得到的值就是函數(shù)方法在緩存中的地址。因此p12就是函數(shù)方法對應(yīng)的bucket地址。

ldp p17, p9, [x12]      // {imp, sel} = *bucket

將bucket裝在到p17和p9中,p17中存放imp,p9中存放key也就是sel。

1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

將找到的sel和傳入的sel進(jìn)行比較,如果相同就表示已經(jīng)找到了執(zhí)行CacheHit,否則執(zhí)行2繼續(xù)查找。

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

在這一步對buckets的首地址p10和我們找到的bucket的地址p12進(jìn)行比較 ,如果不相等則查找前一個bucket,并跳回到1執(zhí)行,否則跳到3執(zhí)行。

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

在這里其實拿到的就是buckets中的第一個bucket,p12 = first bucket。繼續(xù)往下執(zhí)行。接下來的操作其實和上面的執(zhí)行流程是一樣的,唯一不同的是3執(zhí)行的是JumpMiss

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

3、CacheHit

從上面的流程分析我們知道了如果在緩存中找到了和傳入的一直的函數(shù)方法就會執(zhí)行CacheHit。我們來看下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

走這一步是已經(jīng)在緩存找到了相應(yīng)的函數(shù)方法,p17(x17)中存儲了imp,p12(x12)中存放了imp的地址,TailCallCachedImp直接調(diào)用函數(shù)方法。

4、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

走到JumpMiss來則表示在緩存中并沒有找到對應(yīng)的函數(shù)方法,則會跳到__objc_msgSend_uncached執(zhí)行MethodTableLookup。

5、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

MethodTableLookup中的這些操作其實是在從bits中的方法列表去找函數(shù)方法,這篇文章中有分析bits。最終跳到__class_lookupMethodAndLoadCache3去執(zhí)行。從這里開始進(jìn)入到方法的查找流程。

三、方法查找

從上面的objc_msgSend匯編源碼分析來看,當(dāng)在緩存cache中未能命中方法的時候,最終會走到__class_lookupMethodAndLoadCache3。__class_lookupMethodAndLoadCache3對應(yīng)上層C實現(xiàn)的_class_lookupMethodAndLoadCache3方法,該方法定義在objc-runtime-new.mm中。

1、_class_lookupMethodAndLoadCache3方法

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

obj 類的實例對象
sel 方法的名稱
cls 類

_class_lookupMethodAndLoadCache3調(diào)用了lookUpImpOrForward方法,我們看到這的cache傳入的是NO,表示函數(shù)方法沒有緩存命中,resolver是消息的接受者。

2、lookUpImpOrForward方法的準(zhǔn)備工作

lookUpImpOrForward的代碼實現(xiàn)如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
  ...
}

這里的代碼是lookUpImpOrForward函數(shù)方法的部分代碼,這樣一段代碼做了一下幾步準(zhǔn)備工作。

  1. runtimeLock.lock() 加鎖避免在多線程的情況下出現(xiàn)錯亂的情況。
  2. checkIsKnownClass(cls) 判斷class的有效性。
  3. realizeClass(cls)class_rw_tclass_ro_t中加載方法,具體的可以參閱realizeClass方法的實現(xiàn)。

做好上面的準(zhǔn)備工作后,程序會執(zhí)行retry的代碼開始方法的查找。其實這里還是會到類的緩存中再去查找一遍。

3、 再去緩存中查找

為了避免在多線程的情況下可能存在方法緩存慢于方法命中的情況,會再次去緩存中查找一次方法。

imp = cache_getImp(cls, sel);
if (imp) goto done;

在這里cache_getImp其實是匯編中_cache_getImp上層C代碼映射。

STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP

重新走CacheLookup流程從緩存中查找,如果在緩存中有查找到則直接goto done釋放鎖,返回imp,結(jié)束方法調(diào)用,否則會先從本類中開始查找方法。

4、本類中查找

如果方法在緩存中未能找到,會在本類的方法列表中查找方法的實現(xiàn)。

 // Try this class's method lists.
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
    log_and_fill_cache(cls, meth->imp, sel, inst, cls);
    imp = meth->imp;
    goto done;
}

這段是在本類的方法列表中查找方法實現(xiàn)的代碼。調(diào)用getMethodNoSuper_nolock從方法列表中查找,如果查找到則調(diào)用log_and_fill_cache方法進(jìn)行方法的緩存,goto done釋放鎖,返回imp。這里的查找算法是一個二分查找算法。如果本類中沒有方法的實現(xiàn),便會從類的父類中查找方法的實現(xiàn)。

5、父類中查找

如果我們調(diào)用的方法在本類中未能實現(xiàn),則會從父類的方法列表中查找。

 {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);//父類緩存中查找
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {//是否是消息轉(zhuǎn)發(fā)的方法
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

1、先從父類的緩存中查找,如果在緩存中有找到,則會先判斷是否是消息轉(zhuǎn)發(fā)的方法。
2、如果是消息轉(zhuǎn)發(fā)的方法則會走消息轉(zhuǎn)發(fā)的流程,終止方法的查找。
3、如果是非消息轉(zhuǎn)發(fā)的方法則會調(diào)用log_and_fill_cache進(jìn)行方法的緩存,終止方法的查找并執(zhí)行方法。
4、如果在父類的緩存中沒有找到,則會從父類的方法列表中查找,如果找到了則會調(diào)用log_and_fill_cache進(jìn)行方法的緩存,終止方法的查找并執(zhí)行方法。
5、如果在父類的方法列表中沒有找到,重復(fù)執(zhí)行1、3、4步驟,直到父類為nil為止。
6、如果直到父類為nil還是未能找到方法的實現(xiàn),則會走動態(tài)方法解析流程。

四、總結(jié)

1、方法調(diào)用的底層實現(xiàn)是objc_msgSend,即方法的本質(zhì)是消息發(fā)送。
2、objc_msgSend是用匯編實現(xiàn)的。objc_msgSend從緩存中查找方法,如果有查找到就會執(zhí)行方法,否則會去調(diào)用的_class_lookupMethodAndLoadCache3這樣的一個C函數(shù)進(jìn)行方法的查找。
3、_class_lookupMethodAndLoadCache3方法中會做一些準(zhǔn)備的工作,然后會再次匯編查找一次緩存,如果找到就執(zhí)行方法,否則會從本類的方法列表中查找。
4、在本類的方法列表中沒有找到則去父類的緩存中查找,如果有查找到則會判斷是否走消息轉(zhuǎn)發(fā)流程。否則去父類的方法列表中查找。
5、如果在本類緩存、本類方法列表、父類緩存、父類方法列表中都未找到,走動態(tài)方法解析流程。

五、參考資料

匯編指令
方法緩存cache_t
深入OC底層探索NSObject的結(jié)構(gòu)
動態(tài)方法解析和消息轉(zhuǎn)發(fā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 閱讀本文后你將會進(jìn)一步了解Runtime的實現(xiàn),享元設(shè)計模式的實踐,內(nèi)存數(shù)據(jù)存儲優(yōu)化,編譯內(nèi)存屏障,多線程無鎖讀寫...
    歐陽大哥2013閱讀 18,169評論 19 127
  • 動手實現(xiàn) objc_msgSend objc_msgSend 函數(shù)支撐了我們使用 Objective-C 實現(xiàn)的一...
    大鵬你我他閱讀 1,058評論 0 2
  • 技 術(shù) 文 章 / 超 人 Runtime(運(yùn)行時機(jī)制)概念 Object-C 是面向?qū)ο蟮恼Z言,C是面向結(jié)構(gòu)也就...
    樹下敲代碼的超人閱讀 1,091評論 0 16
  • Objc 的方法調(diào)用是運(yùn)行時決定的,系統(tǒng)會根據(jù) selector 動態(tài)地查找 IMP,那么這一過程究竟是怎樣實現(xiàn)的...
    gbupup閱讀 1,105評論 0 11
  • 今晚的月亮是冰山美人,光芒清冷透亮。零星幾顆星星小碎鉆般忽閃忽閃,像忙碌在月亮小姐身邊的小女仆。 幽靈獨自坐在樹梢...
    奔跑的萃萃閱讀 294評論 0 6

友情鏈接更多精彩內(nèi)容