objc_msgSend 源碼閱讀

objc_msgSend是OC中調(diào)用最為頻繁的方法,所有OC方法的調(diào)用都離不開這個(gè)它。蘋果已經(jīng)將其開源(https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/),這是使用匯編語言編寫的,其好處就是能提升函數(shù)的執(zhí)行速度。本文選用它的arm64為匯編代碼(objc-msg-arm64.s)進(jìn)行分析。

函數(shù)入口

首先,找到ENTRY _objc_msgSend這一行,它是objc_msgSend的函數(shù)入口,下面逐行進(jìn)行分析:

cmp p0, #0   將傳入的第一個(gè)參數(shù)與0判斷

這里的p0實(shí)際上就是x0,其定義在arm64-asm.h里面。

    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)

如果p0<0(即最高位為1),該對象是tagged pointer,實(shí)際上是一個(gè)為了節(jié)省空間而使用的特殊指針,關(guān)于它的詳細(xì)描述可以看這篇文章,而當(dāng)p0為0的時(shí)候,即代表傳入對象為nil,函數(shù)應(yīng)該立即返回,總之,都先要跳到LNilOrTagged進(jìn)行特殊處理。。

如果p0>0,則代碼會(huì)繼續(xù)執(zhí)行下去:

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

然后將x0指向內(nèi)存中的值(isa)賦值給p13,然后通過GetClassFromIsa_p16的宏后,p16得到了class的地址。GetClassFromIsa_p16的實(shí)現(xiàn)如下(剔除了SUPPORT_INDEXED_ISA的部分,因?yàn)樗轻槍atch的):

.macro GetClassFromIsa_p16 /* src */
    and p16, $0, #ISA_MASK
#endif

ISA_MASK是定義在isa.h的宏,其值為0x0000000ffffffff8ULL。

可以看出class的地址是isa指針跟ISA_MASK與運(yùn)算得來的,其中的關(guān)系可以參考這篇文章,這里就不展開講了。

LGetIsaDone:
    CacheLookup NORMAL  

接下來,就是查緩存的流程,在講這個(gè)之前,先把其它分支條件過一遍。

LNilOrTagged

LNilOrTagged的實(shí)現(xiàn)723版本和750版本的不太一樣,不過原理是一樣的,先看下723版本的:

LNilOrTagged:
1   b.eq    LReturnZero     // nil check    

    // tagged
2   mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    
3   adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    
4   ubfx    x11, x0, #60, #4
5   ldr x16, [x10, x11, LSL #3]
6   b   LGetIsaDone
  1. 首先,如果p0 = 0,則跳到LReturnZero返回。接下來就是處理tagged pointer的邏輯。
  2. tagged pointer有兩種,一種是系統(tǒng)的,其isa的前4位為標(biāo)志位,最高位位1。另一種是開發(fā)者擴(kuò)展的,其isa的前8位是標(biāo)志位,前4位都是1。因此,如果p0比0xf00....要大(這里是無符號比較),就跳到LExtTag進(jìn)行擴(kuò)展的處理,否則執(zhí)行系統(tǒng)tagged pointer的邏輯。
  3. 取出_objc_debug_taggedpointer_classes的地址加載到x10中
  4. 獲取x0的高4位保存到x11中(高4位也是isa指針在_objc_debug_taggedpointer_classes中的索引)
  5. 以x11作為索引,算出對應(yīng)isa指針的內(nèi)存地址存到x16中
  6. 取出class地址之后執(zhí)行LGetIsaDone

對于750的版本,邏輯是這樣的:

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

750的版本并沒有區(qū)分tagged pointer是系統(tǒng)的還是擴(kuò)展的,直接就將其當(dāng)成系統(tǒng)的處理取出class地址,在這之后,又將___NSUnrecognizedTaggedPointer的地址賦給x10,如果取出的這個(gè)class地址跟NSUnrecognizedTaggedPointer相等,就代表這是一個(gè)擴(kuò)展指針(因?yàn)槿绻菙U(kuò)展指針的話,最高4位必須是1,通過前面的運(yùn)算之后x16存的地址只能是一個(gè)確定的值。也可以由此推斷出___NSUnrecognizedTaggedPointer_objc_debug_taggedpointer_classes中的索引是0x1111。

我看不出750的實(shí)現(xiàn)方式優(yōu)越在哪個(gè)地方,看起來都是9條匯編代碼,希望有大神來解釋一下。

LExtTag

求擴(kuò)展的tagged pointer的class地址和系統(tǒng)的tagged pointer是類似的,其代碼如下:

1   adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    
2   ubfx    x11, x0, #52, #8
    
3   ldr x16, [x10, x11, LSL #3]
    
4   b   LGetIsaDone

  1. 取出_objc_debug_taggedpointer_ext_classes的地址加載到x10中。
  2. 去x0(isa指針)的高8位放到x11
  3. 通過索引求出class的地址并將其放到x16
  4. 執(zhí)行LGetIsaDone

LReturnZero

如果p0=0,則說明傳入的類為nil,這個(gè)時(shí)候應(yīng)該執(zhí)行返回nil的邏輯

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

在arm64位中,函數(shù)整型的返回值會(huì)存在x0,x1中,而浮點(diǎn)數(shù)的返回值存在v1-v3中,由于不知道函數(shù)的調(diào)用者需要什么類型,因此會(huì)將上述寄存器都清空,x0已經(jīng)是0了,因此不需要清空。

CacheLookup

不管是哪個(gè)分支條件,來到CacheLookup這個(gè)宏之后,p16都已經(jīng)得到類的地址了,接下來就是查找緩存的過程

    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask

將class地址的CACHE偏移量的內(nèi)存賦值給p10和p11,對于CACHE的定義可以在本文件中找到:

/* Selected field offsets in class structure */
#define SUPERCLASS       __SIZEOF_POINTER__
#define CACHE            (2 * __SIZEOF_POINTER__)

其中SIZEOF_POINTER是8個(gè)字節(jié),因此這里偏移了16個(gè)字節(jié),在objc-runtime-new.h中,我們可以找到objc_class的實(shí)現(xiàn)如下(注意不是runtime.h里面的objc_class,后者已經(jīng)廢棄掉了):

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

對于其父結(jié)構(gòu)體objc_object,其定義在objc.h

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

這樣,我們可以推斷出isa指針的偏移量是0,superClass的偏移量是8,cache的偏移量是16。

對于cache_t的結(jié)構(gòu)體,定義如下:

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

其中bucket_t占了8字節(jié),而mark_t占了4個(gè)字節(jié),因此ldr p10, p11, [x16, #CACHE]的結(jié)果是p10存了_buckets,p11高32位存_occupied,低32位存_mask(因?yàn)閍rm64默認(rèn)是小端)
_buckets就是存緩存函數(shù)地址的地方,實(shí)際上是一個(gè)哈希表,_mask總是2的n次冪-1,也就是0x00....1111,通過它和函數(shù)方法可以求出函數(shù)在哈希表中的索引。

    and  w12, w1, w11       // x12 = _cmd & mask

通過上面的運(yùn)算,就可以得到函數(shù)方法在哈希表的索引,實(shí)際上就相當(dāng)于_cmd%哈希表的大小,可以看出,也就是說哈希表的構(gòu)造方法是除留余數(shù)法。

    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

PTRSHIFT 的定義在arm64-asm.h,也就是3。p10是_buckets的首地址,因?yàn)閎ucket_t的大小為16個(gè)字節(jié),所以需要將索引乘以16,也就是左移3位。計(jì)算完之后,p12里面就是對應(yīng)的bucket的指針了。

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

將bucket加載到p17和p9,bucket_t的結(jié)構(gòu)如下:

struct bucket_t {
    MethodCacheIMP _imp;
    cache_key_t _key;
    ...

通過這一運(yùn)算,p17存放了_imp,p9存放了_key,而_key實(shí)際上就是sel。

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

找到sel后,就讓傳進(jìn)來的sel和找到的sel作對比,如果一樣,則跳到CacheHit執(zhí)行函數(shù),如果沒找到,可能是出現(xiàn)哈希沖突了,開始繼續(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

CheckMiss的作用是找出來的sel是否為nil,是的話就跳出匯編用C語言的方式找,其實(shí)現(xiàn)一會(huì)再講,如果p12和p10相等,即找到的bucket是buckets的首地址,那就跳到3(跳到最后一個(gè)bucket繼續(xù)查找)如果不是,則倒序查找跳到前一個(gè)bucket,跳回1繼續(xù)查找。

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.

能來到3,說明找到的bucket是buckets的第一個(gè),這個(gè)時(shí)候,跳到最后一個(gè)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

接下來執(zhí)行的1,2跟上面的1,2一樣,不一樣的是,如果再次碰到第一個(gè)bucket,就跳出匯編。

CacheHit的宏如下:

.macro CacheHit
    TailCallCachedImp x17, x12  // authenticate and call imp
.endmacro

對于TailCallCachedImp,它定義在arm64-asm.h

.macro TailCallCachedImp
    // $0 = cached imp, $1 = address of cached imp
    brab    $0, $1
.endmacro

這個(gè)時(shí)候,x12存了IMP的地址,x17存了保存的IMP,但是brab是什么命令我沒查到,大意應(yīng)該就是調(diào)用了這個(gè)緩存的函數(shù)。

總結(jié)一下CacheLookup這個(gè)流程,如果緩存高級語言的寫法,那應(yīng)該就是:

bucket_t bucket = class->cache->buctet[sel]
if (sel == bucket->_key){
    bucket]->_imp()
} else{
    //執(zhí)行C語言的邏輯
}

緩存找不到的case

不管是JumpMiss還是CheckMiss的時(shí)候sel為空(也就是沒有找到緩存的sel)最后都會(huì)來到
_class_lookupMethodAndLoadCache3這個(gè)C函數(shù)這個(gè)函數(shù)定義在objc-runtime-new.mm,代碼如下:

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

這個(gè)方法返回查找過后IMP指針,供匯編代碼調(diào)用,這里就不貼出返回之后的邏輯了。而lookUpImpOrForward是一個(gè)查找方法的函數(shù)

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

    runtimeLock.assertUnlocked();
    
    //因?yàn)閭魅氲腸ache為NO,所以不會(huì)執(zhí)行(匯編已經(jīng)執(zhí)行過一遍了)
    if (cache) { 
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.lock();
    
    //檢查類是否是已知的類,如果是NSClassFromString()方法得到的,那有可能是未知的
    checkIsKnownClass(cls);

    // 判斷類是否已經(jīng)實(shí)現(xiàn),如果沒有先將其實(shí)現(xiàn)
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    //檢查類是否被初始化,如果沒有,則將其初始化
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
    //嘗試在緩存里找
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 在本類的方法列表中查找
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 在父類中查找,也是先找緩存,再找方法列表,如果找到,則將該方法緩存到該類中
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                    //判斷這是不是消息轉(zhuǎn)發(fā)的方法
                if (imp != (IMP)_objc_msgForward_impcache) {
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    //如果是消息轉(zhuǎn)發(fā),先不調(diào)用
                    //先調(diào)用resolveInstanceMethod
                        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;
            }
        }
    }

    // 如果沒找到實(shí)現(xiàn),則調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel
    //和+ (BOOL)resolveInstanceMethod:(SEL)sel方法,重新試一次

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }
    
    //如果還是找不到,走消息轉(zhuǎn)發(fā)
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

這個(gè)方法其實(shí)就是在類中查找方法的整套流程實(shí)現(xiàn)。這個(gè)過程是線程安全的。找到的方法都會(huì)調(diào)用cache_fill存到緩存里面。一旦方法被緩存起來,下次調(diào)用的時(shí)候則只需要執(zhí)行匯編的代碼就可以找到方法。大大地提高代碼執(zhí)行的效率。

參考文獻(xiàn)

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

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

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