oc-底層原理之objc_msgSend方法快速查找

oc-底層原理分析之Cache_t一文中我們對(duì)方法的緩存進(jìn)行了探討,這篇文章我們?cè)趤硌芯恳幌?code>方法的查找

方法的查找有兩條線路:

  1. 快速查找(通過匯編實(shí)現(xiàn))
  2. 慢速查找(通過c實(shí)現(xiàn))(下一篇文章再來探究)

方法快速查找

方法的快速查找實(shí)際是通過緩存來查找,在探究之前,我們先來了解一下objc_msgSend,我們要知道方法的查找是在什么時(shí)機(jī)通過什么入口進(jìn)入的

objc_msgSend

我們知道objective-c是一門動(dòng)態(tài)語言,所有的方法調(diào)用并不是在編譯階段就確定的,當(dāng)我們通過sel去查找imp的時(shí)候是在運(yùn)行時(shí)才具體確定sel所對(duì)應(yīng)的imp地址的,我們來看看下面的例子:
先定義兩個(gè)類WPersonWTeacher,WTeacher繼承自WPerson

@interface WPerson : NSObject
- (void)sayHello;
@end

@implementation WPerson
- (void)sayHello{
    NSLog(@"hello");
}
@end

@interface WTeacher : WPerson
- (void)sayHello;
- (void)sayNB;
@end

@implementation WTeacher
- (void)sayNB{
    NSLog(@"666");
}
@end

如果我們要調(diào)用sayNB方法,我們可以使用WTeacher的對(duì)象來調(diào)用:

WTeacher *teacher = [WTeacher alloc];
[teacher sayNB];

類的結(jié)構(gòu)分析一文中我們通過Clang將類編譯成.cpp文件后,我們可以看到類的結(jié)構(gòu)中,方法的調(diào)用為:

WTeacher *teacher = ((WTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacter, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacter, sel_registerName("sayHello"));

我們可以看到任何方法的調(diào)用都是通過objc_msgSend來給對(duì)象發(fā)送消息的方式實(shí)現(xiàn),那么我們直接調(diào)用objc_msgSend

objc_msgSend(teacher,sel_registerName("sayNB"));

我們也能實(shí)現(xiàn)方法的調(diào)用,但是如果當(dāng)我們通過WTeacher調(diào)用sayHello方法的時(shí)候,實(shí)際上調(diào)用的是父類的sayHello方法,所以我們也可以直接向發(fā)送消息:

struct objc_super tsuper;
tsuper.receiver = teacher;
tsuper.super_class = [WPerson class];
    
objc_msgSendSuper(&tsuper, sel_registerName("sayHello"));

你也可以自己嘗試一下。我們可以得出一個(gè)結(jié)論,

使用oc對(duì)象或者類來調(diào)用方法時(shí)實(shí)際上是通過objc_msgSend向?qū)ο蟀l(fā)送消息

objc_msgSend源碼分析

objc_msgSend調(diào)用方式為:

objc_msgSend(teacher,sel_registerName("sayNB"));

第一個(gè)參數(shù)為:消息接受者
第二個(gè)參數(shù)為:消息的sel

接下來我們研究一下objc_msgSend源碼:

objc_msgSend源碼有多個(gè)版本,armarm64,i386,模擬器,我們這里都以arm64為例講解

arm64objc_msgSend的源碼在objc-msg-arm64.s文件中

objc_msgSend
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
   // p0是第一個(gè)參數(shù):消息接受者
   // 這里先比較p0是否為空,如果為空,則直接返回
    cmp p0, #0  
    //是否支持  TAGGED_POINTERS
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
   //消息接受者為空,直接返回空
    b.eq    LReturnZero
#endif
  //消息接受者不為空
  //p13 獲取消息接受者的isa并賦值給p13
    ldr p13, [x0]       // p13 = isa
    //根據(jù)獲取到的isa獲取class并賦值給p16
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    //isa獲取結(jié)束,開始在cache中查找imp
    CacheLookup NORMAL, _objc_msgSend
CacheLookup

要理解這部分源碼,需要先理解什么是cache_t,我們已經(jīng)在oc-底層原理分析之Cache_t一文中進(jìn)行了詳細(xì)的探索,請(qǐng)先閱讀這一部分

.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LLookupRecover$1 which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

    // p1 = SEL, p16 = isa
    // #define CACHE (2 * __SIZEOF_POINTER__) 
    // x16存儲(chǔ)的是isa,平移16個(gè)字節(jié)后得到cache_t并賦值給p11
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
   // 獲取buckets p11&0x0000ffffffffffff得到后48位,并賦值給p10
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    // 獲取hash,邏輯右移48位得到mask
    // 然后p1&mask 得到hash的key,并賦值給p12
    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

  // p12當(dāng)前存儲(chǔ)的是hash key,先邏輯左移4位然后再和p10相與,得到對(duì)應(yīng)的bucket并保存在p12中
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
  //將p12屬性的imp和sel分別賦值給p17和p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    //判斷當(dāng)前sel和傳入的sel是否相等
1:  cmp p9, p1          // if (bucket->sel != _cmd)
  //如果不相等,跳入2f
    b.ne    2f          //     scan more
  //如果相等,跳入CacheHit
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    // 判斷p12和p10是否相等 
    cmp p12, p10        // wrap if bucket == buckets
    //如果相等,跳入3f
    b.eq    3f
    //如果不相等,反向循環(huán)查找
    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
  //將p12指向buckets的最后一個(gè)元素
    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

    // 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
CacheLookup源碼詳解

首先我們要知道類的結(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

    class_rw_t *data() const {
        return bits.data();
    }
    
    //這里的其他方法以及屬性已經(jīng)略去
}

我們知道objc_class中有以下幾個(gè)屬性:

  1. ISA
  2. superclass
  3. cache
  • ldr p11, [x16, #CACHE]p16存儲(chǔ)的是isa,CACHE為16個(gè)字節(jié),p16平移16字節(jié)后就得到cache的地址,所以此時(shí)p11存儲(chǔ)的是cache地址

  • and p10, p11, #0x0000ffffffffffff:在cache_t中,低48位存儲(chǔ)的是buckets,高16位存儲(chǔ)的是mask,用cache指針和0x0000ffffffffffff進(jìn)行與運(yùn)算以后,就得到低48位。也就是buckets,所以此時(shí)p10 = buckets

  • and p12, p1, p11, LSR #48:p11(cache)指針邏輯右移48位得到mask,然后再和p1(sel)相與,得到hash key
    要理解這一步,就需要了解cache的存儲(chǔ),我們 先看cache的insert方法中獲取hash key的方法:

    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask;
    }
    

    我們理解了插入時(shí)如何生產(chǎn)hash key,那么這一 步也不難理解

  • add p12, p10, p12, LSL #(1+PTRSHIFT):PTRSHIFT = 3,p12當(dāng)前是存儲(chǔ)的hash key(實(shí)際上相當(dāng)于index),bucket機(jī)構(gòu)體中,包含兩個(gè)元素 selimp,占用16個(gè)字節(jié),p12邏輯左移4位,相當(dāng)于 index * 16,然后p10再平移index * 16,得到對(duì)應(yīng)的bucket,此時(shí)p12存儲(chǔ)的是對(duì)應(yīng)的buckets

  • add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)):在循環(huán)查找中,如果當(dāng)前bucket已經(jīng)指向了cache的首地址(也就是buckets的地址),那么說明循環(huán)結(jié)束了,此時(shí)需要將p12指向buckets的最后一個(gè)元素

通過以上的快速查找流程,如果沒有查到對(duì)應(yīng)的imp,還會(huì)經(jīng)過的慢速查找,關(guān)于慢速查找,下一篇文章會(huì)有介紹

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

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