objc_msgSend 源碼閱讀

objc_msgSend是OC中調(diào)用最為頻繁的方法,所有OC方法的調(diào)用都離不開這個(gè)它选侨。蘋果已經(jīng)將其開源(https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/)傻寂,這是使用匯編語(yǔ)言編寫的,其好處就是能提升函數(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),該對(duì)象是tagged pointer币喧,實(shí)際上是一個(gè)為了節(jié)省空間而使用的特殊指針,關(guān)于它的詳細(xì)描述可以看這篇文章袱耽,而當(dāng)p0為0的時(shí)候杀餐,即代表傳入對(duì)象為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必峰,然后通過(guò)GetClassFromIsa_p16的宏后,p16得到了class的地址钻蹬。GetClassFromIsa_p16的實(shí)現(xiàn)如下(剔除了SUPPORT_INDEXED_ISA的部分,因?yàn)樗轻槍?duì)watch的):

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

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

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

LGetIsaDone:
    CacheLookup NORMAL  

接下來(lái)顺献,就是查緩存的流程旗国,在講這個(gè)之前,先把其它分支條件過(guò)一遍注整。

LNilOrTagged

LNilOrTagged的實(shí)現(xiàn)723版本和750版本的不太一樣能曾,不過(guò)原理是一樣的,先看下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返回。接下來(lái)就是處理tagged pointer的邏輯椒袍。
  2. tagged pointer有兩種蚂斤,一種是系統(tǒng)的,其isa的前4位為標(biāo)志位槐沼,最高位位1曙蒸。另一種是開發(fā)者擴(kuò)展的,其isa的前8位是標(biāo)志位岗钩,前4位都是1纽窟。因此,如果p0比0xf00....要大(這里是無(wú)符號(hào)比較)兼吓,就跳到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作為索引视搏,算出對(duì)應(yīng)isa指針的內(nèi)存地址存到x16中
  6. 取出class地址之后執(zhí)行LGetIsaDone

對(duì)于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的版本并沒(méi)有區(qū)分tagged pointer是系統(tǒng)的還是擴(kuò)展的,直接就將其當(dāng)成系統(tǒng)的處理取出class地址浑娜,在這之后佑力,又將___NSUnrecognizedTaggedPointer的地址賦給x10,如果取出的這個(gè)class地址跟NSUnrecognizedTaggedPointer相等筋遭,就代表這是一個(gè)擴(kuò)展指針(因?yàn)槿绻菙U(kuò)展指針的話打颤,最高4位必須是1暴拄,通過(guò)前面的運(yùn)算之后x16存的地址只能是一個(gè)確定的值。也可以由此推斷出___NSUnrecognizedTaggedPointer_objc_debug_taggedpointer_classes中的索引是0x1111编饺。

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

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. 通過(guò)索引求出class的地址并將其放到x16
  4. 執(zhí)行LGetIsaDone

LReturnZero

如果p0=0秽誊,則說(shuō)明傳入的類為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è)分支條件耘纱,來(lái)到CacheLookup這個(gè)宏之后,p16都已經(jīng)得到類的地址了毕荐,接下來(lái)就是查找緩存的過(guò)程

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

將class地址的CACHE偏移量的內(nèi)存賦值給p10和p11束析,對(duì)于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
    ...

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

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

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

對(duì)于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,通過(guò)它和函數(shù)方法可以求出函數(shù)在哈希表中的索引豺妓。

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

通過(guò)上面的運(yùn)算飒赃,就可以得到函數(shù)方法在哈希表的索引利花,實(shí)際上就相當(dāng)于_cmd%哈希表的大小,可以看出载佳,也就是說(shuō)哈希表的構(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里面就是對(duì)應(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;
    ...

通過(guò)這一運(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)來(lái)的sel和找到的sel作對(duì)比阐枣,如果一樣马靠,則跳到CacheHit執(zhí)行函數(shù),如果沒(méi)找到蔼两,可能是出現(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的作用是找出來(lái)的sel是否為nil,是的話就跳出匯編用C語(yǔ)言的方式找额划,其實(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.

能來(lái)到3储笑,說(shuō)明找到的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

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

CacheHit的宏如下:

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

對(duì)于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是什么命令我沒(méi)查到澎嚣,大意應(yīng)該就是調(diào)用了這個(gè)緩存的函數(shù)疏尿。

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

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

緩存找不到的case

不管是JumpMiss還是CheckMiss的時(shí)候sel為空(也就是沒(méi)有找到緩存的sel)最后都會(huì)來(lái)到
_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è)方法返回查找過(guò)后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í)行過(guò)一遍了)
    if (cache) { 
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

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

    // 判斷類是否已經(jīng)實(shí)現(xiàn),如果沒(méi)有先將其實(shí)現(xiàn)
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    //檢查類是否被初始化诫龙,如果沒(méi)有析显,則將其初始化
    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;
            }
        }
    }

    // 如果沒(méi)找到實(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è)過(guò)程是線程安全的。找到的方法都會(huì)調(diào)用cache_fill存到緩存里面伶选。一旦方法被緩存起來(lái)史飞,下次調(diào)用的時(shí)候則只需要執(zhí)行匯編的代碼就可以找到方法。大大地提高代碼執(zhí)行的效率仰税。

參考文獻(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末构资,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子陨簇,更是在濱河造成了極大的恐慌吐绵,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件河绽,死亡現(xiàn)場(chǎng)離奇詭異己单,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)耙饰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門纹笼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人苟跪,你說(shuō)我怎么就攤上這事廷痘÷В” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵笋额,是天一觀的道長(zhǎng)元暴。 經(jīng)常有香客問(wèn)我,道長(zhǎng)鳞陨,這世上最難降的妖魔是什么昨寞? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮厦滤,結(jié)果婚禮上援岩,老公的妹妹穿的比我還像新娘。我一直安慰自己掏导,他們只是感情好享怀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趟咆,像睡著了一般添瓷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上值纱,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天鳞贷,我揣著相機(jī)與錄音,去河邊找鬼虐唠。 笑死搀愧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疆偿。 我是一名探鬼主播咱筛,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼杆故!你這毒婦竟也來(lái)了迅箩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤处铛,失蹤者是張志新(化名)和其女友劉穎饲趋,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撤蟆,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奕塑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了枫疆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爵川。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖息楔,靈堂內(nèi)的尸體忽然破棺而出寝贡,到底是詐尸還是另有隱情扒披,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布圃泡,位于F島的核電站碟案,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏颇蜡。R本人自食惡果不足惜价说,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望风秤。 院中可真熱鬧鳖目,春花似錦、人聲如沸缤弦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)碍沐。三九已至狸捅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間累提,已是汗流浹背尘喝。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斋陪,地道東北人朽褪。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鳍贾,于是被迫代替她去往敵國(guó)和親鞍匾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子交洗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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