解讀objc_msgSend

引入

眾所周知,Objective-C動態(tài)性的根源在方法的調(diào)用是通過message來實現(xiàn)的涝婉,一次發(fā)生message的過程就是一次方法的調(diào)用過程劲件。發(fā)送message只需要指定對象和SEL未巫,Runtime的objc_msgSend會根據(jù)在信息在對象isa指針指向的Class中尋找該SEL對應的IMP,從而完成方法的調(diào)用蔗草。這樣每次方法的調(diào)用必然會有方法的查找過程暇榴,如果頻繁調(diào)用,或者Class的方法列表過大蕉世,很容易導致性能瓶頸蔼紧,但OC似乎并沒有這個問題,這得益于蘋果的優(yōu)化機制狠轻,其中包括純匯編的objc_msgSend實現(xiàn)(不用匯編參數(shù)暫存困難奸例,當然也不是沒有辦法,總有一些歪招可以解決向楼,但為了兼顧效率自然匯編更加合適)查吊,方法查找cache,TaggedPointer等等技術才帶來OC極高的效率湖蜕。接下來我們從objc_msgSend為引逻卖,來解讀整個過程。

Objective-C動態(tài)化的核心objc_msgSend

先說幾句閑話昭抒,如果大學期間學習過匯編課程的同學就知道评也,相同的邏輯,c語言寫出的函數(shù)匯編成.s文件和直接匯編的的文件灭返,體積差異是很大的盗迟,幾倍到幾十倍的差距,由此可見兩者的效率差距也十分巨大熙含,這也是蘋果為什么非要用匯編去干這個事情罚缕。而且蘋果匯編只實現(xiàn)了cache方法的查找過程,并未匯編實現(xiàn)class所有的方法查找怎静,因為前者調(diào)用非常頻繁邮弹,后者卻不是黔衡,其在運行效率和開發(fā)效率(可靠性)作了一個平衡。

進入正題腌乡,我們知道對于任意的OC方法的調(diào)用盟劫,比如[obj aMethod];都會被翻譯成objc_msgSend(obj, sel/*@selector(aMethod)*/);,由此進入objc_msgSend執(zhí)行导饲,而該方法實現(xiàn)是匯編完成的捞高,這對解讀造成了一定的困擾妥粟,所以我們不得不迎難而上变过,搞定整個方法的邏輯過程男韧。

我這里下載的objc_706的源碼,這里我只解讀objc-msg-arm64.s文件袋毙,其他處理器架構(gòu)的除了實現(xiàn)細節(jié)出入,其查找邏輯的類似冗尤。

文件的最開始聲明了12個私有的_objc_entryPoints听盖,其中包括我們關注的.quad _objc_msgSend

在文件中搜索"_objc_msgSend",會找到以下匯編代碼裂七,這就是其實現(xiàn)的一部分皆看,接下來我們將一步步解讀它。


    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    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]
    b   LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
    
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend
數(shù)據(jù)結(jié)構(gòu)定義

咋一看背零,只能了解其定義了一些數(shù)據(jù)存儲的空間腰吟,里面存儲的應該是指針,而且這些指針3bit對齊徙瓶,似乎跟OC的objc_object指針很類似毛雇,然后通過.fill偽指令,將所有的數(shù)據(jù)單元填0侦镇。至于這些數(shù)據(jù)怎么使用我們并不了解灵疮,沒關系,我們繼續(xù)往下看壳繁。

找到了ENTRY _objc_msgSend震捣,顧名思義這就是真正的函數(shù)入口了。我們搜索ENTRY闹炉,其定義如下:

.macro ENTRY /* name */
    .text
    .align 5
    .globl    $0
$0:
.endmacro

這里定義了一個匯編宏伍派,表示text段,定義一個global的_objc_msgSend剩胁,"$0"同時生產(chǎn)一個函數(shù)入口標簽诉植。

UNWIND _objc_msgSend, NoFrame則定義了一些段存儲數(shù)據(jù)對象,簡單來說就是類似于結(jié)構(gòu)體數(shù)據(jù)對象昵观,具體意義我也不是很了解晾腔。

MESSENGER_START定義了一些方法調(diào)用開始的調(diào)試數(shù)據(jù)舌稀,具體對應到objc-gdb.h中的

#define OBJC_MESSENGER_START    1
#define OBJC_MESSENGER_END_FAST 2
#define OBJC_MESSENGER_END_SLOW 3
#define OBJC_MESSENGER_END_NIL  4

struct objc_messenger_breakpoint {
    uintptr_t address;
    uintptr_t kind;
};

函數(shù)邏輯主體

邏輯部分有很多子邏輯,我們一步一步解讀

tagged pointer處理

cmp x0, #0灼擂,從注釋可以了解是在和"0"比較壁查,比較的結(jié)果會有三種,大于小于等于剔应。

  1. 這里邏輯是b.le LNilOrTagged睡腿,即如果小于等于就跳轉(zhuǎn)到標簽:LNilOrTagged。(因為nil==0峻贮,tagged指針最高位是1(符號位)席怪,所以肯定小于0)。

  2. 跳轉(zhuǎn)到LNilOrTagged后纤控,執(zhí)行b.eq LReturnZero繼續(xù)檢查比較結(jié)果挂捻,相等則跳轉(zhuǎn)標簽:LReturnZero

  3. 在LReturnZero中,將x1置為0船万,浮點寄存器d1,d2,d3,d4全部置為0刻撒。這是因為objc_msgSend并不知道,該函數(shù)調(diào)用期望返回的是什么數(shù)據(jù)類型耿导,可能是浮點声怔,整型,指針舱呻,甚至可能結(jié)構(gòu)體醋火,所以其將常用的返回值的寄存器,全部清0狮荔。但對于復雜的結(jié)構(gòu)體胎撇,objc_msgSend就無能為力了(因為其不知道這些數(shù)據(jù)的大小)殖氏,它只能將返回結(jié)果放入x8寄存器晚树,由另外代碼去清0,而這部分代碼則編譯器在編譯的時候根據(jù)相關數(shù)據(jù)類型生成雅采。

  4. 如果b.eq LReturnZero不成立爵憎,則表明該數(shù)據(jù)是個tagged pointer,需要進一步處理才能做調(diào)用婚瓜。

    mov x10, #0xf000000000000000; cmp x0, x10宝鼓,這兩句就很明顯了。比較其最高的4bit巴刻,這應該是個標記位愚铡。我們?nèi)ニ阉髌湎嚓P的定義

    
    #define _OBJC_TAG_INDEX_MASK 0x7
    // array slot includes the tag bit itself
    #define _OBJC_TAG_SLOT_COUNT 16
    #define _OBJC_TAG_SLOT_MASK 0xf
    
    #define _OBJC_TAG_EXT_INDEX_MASK 0xff
    // array slot has no extra bits
    #define _OBJC_TAG_EXT_SLOT_COUNT 256
    #define _OBJC_TAG_EXT_SLOT_MASK 0xff
    
    #if OBJC_MSB_TAGGED_POINTERS
    #   define _OBJC_TAG_MASK (1ULL<<63)
    #   define _OBJC_TAG_INDEX_SHIFT 60
    #   define _OBJC_TAG_SLOT_SHIFT 60
    #   define _OBJC_TAG_PAYLOAD_LSHIFT 4
    #   define _OBJC_TAG_PAYLOAD_RSHIFT 4
    #   define _OBJC_TAG_EXT_MASK (0xfULL<<60)
    #   define _OBJC_TAG_EXT_INDEX_SHIFT 52
    #   define _OBJC_TAG_EXT_SLOT_SHIFT 52
    #   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
    #   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
    #else
    ...//其他
    #endif
    

    其中#0xf000000000000000就是_OBJC_TAG_EXT_MASK (0xfULL<<60)。當然肯定不是一下就找到這里了,是通過isTaggedPointer()沥寥,找到_objc_isTaggedPointer()碍舍,發(fā)現(xiàn)tagged指針相關的操作這里都有,其中_objc_makeTaggedPointer的功能是實現(xiàn)原始數(shù)據(jù)封裝成tagged指針邑雅。

tagged_pointer.png

這里我們畫了一個簡圖片橡,是arm64下的存儲結(jié)構(gòu)(其他CPU下并不一樣,比如X86_64淮野,data存在前部捧书,tag存后部),如果是個擴展的tagged指針骤星,其中0-51位是數(shù)據(jù)部分经瓷,52-59這8個bit是擴展tagged部分,60到62的3bit是tagged妈踊,63是tagged指針標記了嚎。如果是一個tagged指針0-59是數(shù)據(jù)泪漂,60-62是tagged index廊营,63是標記位。所以tagged指針記錄了data+index萝勤。

常見的tagged指針有

    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 

其中tag是一個index露筒,表示_objc_debug_taggedpointer_classes偏移量,而ext_tag是_objc_debug_taggedpointer_ext_classesindex敌卓。

在objc-object.h文件中有以下聲明慎式,所以這兩個數(shù)據(jù)是匯編語言定義,但C++也在聲明和使用趟径。

#if SUPPORT_TAGGED_POINTERS

extern "C" { 
    extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT*2];
    extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
#define objc_tag_classes objc_debug_taggedpointer_classes
#define objc_tag_ext_classes objc_debug_taggedpointer_ext_classes

#endif

?

接著看

. b.hs LExtTag瘪吏,如果比較結(jié)果是大于等于,則表示這是個擴展的tagged蜗巧,跳轉(zhuǎn)到標簽:LExtTag

  1. 接來兩句是加載_objc_debug_taggedpointer_ext_classes

. ubfx x11, x0, #52, #8 的意思是x11= (x0 & 0x0ff0000000000000)>>52掌眠,即取第52-59bit的數(shù)據(jù)。

. ldr x16, [x10, x11, LSL #3]幕屹,x16=x10+(x11<<3)蓝丙,左移三位是因為_objc_debug_taggedpointer_ext_classes是8個byte為單位來偏移的,后面做匯編逆向源碼的時候會有類似的例子來說明望拖。

  1. 如果第五步不成立渺尘,則執(zhí)行按照正常的tagged指針處理,加載_objc_debug_taggedpointer_classes说敏,取出第60-63bit鸥跟,左移三位,取出真正的isa盔沫。

  2. 跳轉(zhuǎn)LGetIsaDone医咨,其使用了匯編宏CacheLookup蚂夕,參數(shù)NORMAL,其用于搜索緩存腋逆。

到這里婿牍,匯編的第一部分就解讀完成了,其主要就是解析當前指針得到對應的class以備后續(xù)處理惩歉。為了更好的理解等脂,這里我貼出逆向出來的源碼,以便于理解撑蚌。

id objc_msgSend_c(id obj, SEL sel,...) {
    id localObj = obj;
    int64_t obj_i = (int64_t)obj;
    //這一部分處理tagged pointer的isa指針
    if (obj_i == 0) return nil;
    if (obj_i < 0) {
        //tagged pointer
        uintptr_t obj_ui = (uintptr_t)obj_i;
        if (obj_ui >= _OBJC_TAG_EXT_MASK) {
            uint16_t index = (obj_ui << _OBJC_TAG_PAYLOAD_LSHIFT) >> (_OBJC_TAG_EXT_INDEX_SHIFT + _OBJC_TAG_PAYLOAD_LSHIFT);
            localObj = objc_tag_ext_classes[index];
        } else {
            uint16_t index = obj_ui >> _OBJC_TAG_INDEX_SHIFT;
            localObj = objc_tag_classes[index];
        }
    }
    ...
}
核心代碼——緩存查找

這里先給出CacheLookup匯編源碼如下:

.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

這段匯編注釋很詳細上遥,很多給出了對應的c代碼,所以很容易了解其大概做了什么邏輯争涌,但要看懂具體細節(jié)粉楚,并逆向出源碼,對于不常玩匯編的人來說亮垫,還是有那么一點困難模软。

  1. 之前操作,已經(jīng)在x1和x16中存入處理好的相關數(shù)據(jù)饮潦,x0=obj燃异,x1=SEL,x16=isa继蜡。立即數(shù)#CACHE=16回俐,OC對象的內(nèi)存布局中,前面分別是isa和superclass指針稀并,x16+16就是cache的地址仅颇。cache結(jié)構(gòu)體數(shù)據(jù)布局如下。

    typedef uint32_t mask_t;
    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    }
    

    所以加載x16+16后碘举,x10=_buckets忘瓦,x11=_mask和_occupied

  2. 小端機,_mask在x11的低位殴俱,即w11政冻。x1存的是SEL的地址,將其低位w1取出(ARM64下线欲,這里的指針的低32bit是真實地址明场,高32bit一般是1)。這里取出w1與w11做與運算李丰,放入寄存器x12苦锨。

  3. add x12, x10, x12, LSL #4,這句的意思是x12=x10+(x12<<4)。x10是_buckets的首地址舟舒。

    把這句逆向成c代碼如下:

Class cls = localObj->ISA();
cache_t cache = cls->cache;
uintptr_t sel_i = (uintptr_t)sel;
    
bucket_t *bucket = (bucket_t *)((uintptr_t)cache.buckets() + ((sel_i & cache.mask()) << 4));

我們再看看bucket_t的定義

typedef uintptr_t cache_key_t;
struct bucket_t {
    cache_key_t _key;
    IMP _imp;
}

所以x12<<4拉庶,放大了16倍,也就是一個bucket_t的大小秃励。所以我們可以將上句代碼簡化如下:

bucket_t *bucket = &(cache.buckets()[sel_i & cache.mask()])

  1. ldp x9, x17, [x12]氏仗,加載bucket的數(shù)據(jù)到x9和x17。

  2. cmp x9, x1夺鲜,比較x9與x1皆尔,也就是bucket->_key與SEL。

  3. b.ne 2f如果不相等跳轉(zhuǎn)到標簽2

  4. 接上面一步币励,如果相等則CacheHit $0慷蠕,表示命中緩存,CacheHit是一個宏食呻,“$0”是第一個參數(shù)流炕,就是之前的NORMAL。這是將執(zhí)行br x17仅胞,跳轉(zhuǎn)到寄存器x17的地址每辟,也即是bucket->_imp。貼一下這部分代碼吧饼问。

    #define NORMAL 0
    #define GETIMP 1
    #define LOOKUP 2
    
    .macro CacheHit
    .if $0 == NORMAL
     MESSENGER_END_FAST
     br  x17         // call imp
    .elseif $0 == GETIMP
     mov x0, x17         // return imp
     ret
    .elseif $0 == LOOKUP
     ret             // return imp via x17
    .else
    .abort oops
    .endif
    .endmacro
    
    .macro CheckMiss
     // miss if bucket->sel == 0
    .if $0 == GETIMP
     cbz x9, LGetImpMiss
    .elseif $0 == NORMAL
     cbz x9, __objc_msgSend_uncached
    .elseif $0 == LOOKUP
     cbz x9, __objc_msgLookup_uncached
    .else
    .abort oops
    .endif
    .endmacro
    
    .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
    
  5. 如果第一次沒有找到該緩存那么就調(diào)用宏CheckMiss $0影兽,也就是執(zhí)行cbz x9, __objc_msgSend_uncached揭斧,嘛意思呢莱革,就是x9和0比較,如果相等則跳轉(zhuǎn)__objc_msgSend_uncached讹开,其內(nèi)部實現(xiàn)主要是調(diào)用 __class_lookupMethodAndLoadCache3盅视,這個是c代碼實現(xiàn)的,后面再說旦万。

  6. cmp x12, x10闹击,這里是比較cache.buckets()和當前指向的bucket比較,看是否是一樣成艘。

  7. b.eq 3f 相等跳轉(zhuǎn)到標簽3赏半,否則順序執(zhí)行下一指令。

  8. ldp x9, x17, [x12, #-16]!就是將bucket自減淆两,取下一條數(shù)據(jù)断箫,跳轉(zhuǎn)到標簽1循環(huán)執(zhí)行

  9. 接步驟9,x12=x12+w11<<4秋冰,并進入下一部分代碼仲义。可以發(fā)現(xiàn)與之前的代碼幾乎一致,除了末尾的JumpMiss埃撵,也就是出口赵颅。

雖然解讀了代碼,但具體是在做什么邏輯暂刘,可能還是不太明白饺谬,需要說明一下。

首先_buckets是一個簡單的hash表谣拣,就是數(shù)據(jù)結(jié)構(gòu)課上講的那種最基本hash表商蕴,hash值計算公式就是最簡單的hash=sel地址%mask,其中mask就是存儲空間的大小芝发,初始大小是4绪商,如果不夠用時(使用空間大于總空間的3/4)則增長一倍。根據(jù)sel地址計算出的hash值作為偏移量存儲IMP辅鲸。

有了這個基礎格郁,再回顧上面的代碼邏輯。

如果當前sel的地址與存儲的bucket->sel一樣独悴,那就表示已經(jīng)有緩存了例书,直接調(diào)用即可。否則檢查bucket->sel是否為0刻炒,如果為0則表明肯定還沒有建立緩存决采,則直接調(diào)用c代碼建立緩存。如果不等于0坟奥,則表示此處被其他的sel占用了树瞭,這時候就需要通過逐項搜索檢查是否已經(jīng)緩存(因為已計算了index,所以搜索距離會大幅減少)爱谁,同時檢查bucket是不是已經(jīng)移動到最開始晒喷,如果不是則移動指針查找下一個bucket,否則將bucket直接跳轉(zhuǎn)到最末尾繼續(xù)查找访敌。還是畫個圖吧凉敲,這樣就清晰了。
需要注意的是這里用了一個小技巧寺旺,bucket的查找是反向的爷抓,這樣可以不需要知道bucket具體大小,就可以判斷是否已經(jīng)查找完前部阻塑,然后跳轉(zhuǎn)到后部蓝撇。

search_cache.png

了解了上面的邏輯,可以逆向出的C代碼如下:

id objc_msgSend_c(id obj, SEL sel) {
    
    id localObj = obj;
    int64_t obj_i = (int64_t)obj;
    
    if (obj_i == 0) return nil;

    if (obj_i < 0) {
        //tagged pointer
        uintptr_t obj_ui = (uintptr_t)obj_i;
        if (obj_ui >= _OBJC_TAG_EXT_MASK) {
            uint16_t index = (obj_ui << _OBJC_TAG_PAYLOAD_LSHIFT) >> (_OBJC_TAG_EXT_INDEX_SHIFT + _OBJC_TAG_PAYLOAD_LSHIFT);
            localObj = objc_tag_ext_classes[index];
        } else {
            uint16_t index = obj_ui >> _OBJC_TAG_INDEX_SHIFT;
            localObj = objc_tag_classes[index];
        }
    }
    
    Class cls = localObj->ISA();
    cache_t cache = cls->cache;
    uintptr_t sel_i = (uintptr_t)sel;
    bucket_t *bucket = &(cache.buckets()[sel_i & cache.mask()]);
        
    do {
        if (bucket->key() == sel_i) {
            return (id)bucket->imp();
        }
        if (bucket->key() == 0) {
            //調(diào)用匯編方法__objc_msgSend_uncached();
            //其直接調(diào)用了c方法__class_lookupMethodAndLoadCache3
        }
        
    } while((cache.buckets() == bucket) ?
            bucket = &(cache.buckets()[cache.mask()])
            : --bucket);
    
    return nil;
}

可以看出objc_msgSend只用匯編寫了很少的代碼叮姑,只包含tagged指針處理和方法緩存查找唉地,但是其帶來的效率提高卻是巨大的据悔,非常符合28原則,80%情況下調(diào)用了20%代碼耘沼,蘋果就是在這20%的代碼上盡可能的提高效率极颓,帶來明顯的收益。

緩存的建立

以上就是是緩存的查找邏輯群嗤,那么究竟是否正確菠隆,我們需要找到緩存的建立邏輯相互印證,才能得出結(jié)論狂秘。

進入之前說到的__objc_msgSend_uncached骇径,其就兩句MethodTableLookup; br x17,而前一句里面則直接跳轉(zhuǎn)bl __class_lookupMethodAndLoadCache3者春,其緩存加載的主線調(diào)用邏輯如下(其他邏輯暫時先不關注)

lookUpImpOrForward -> log_and_fill_cache->cache_fill -> cache_fill_nolock

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}
  1. key就是sel的地址破衔。
  2. 調(diào)用cache->find查找緩存,如果沒有找到钱烟,則添加新緩存晰筛,調(diào)用incrementOccupied將occupied++;
  3. 只要調(diào)用了本函數(shù)拴袭,不管有沒有找到读第,都把原緩存覆蓋掉。

那么find是怎么完成的呢拥刻?相關代碼如下

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;

    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
}

static inline mask_t cache_hash(cache_key_t key, mask_t mask) {
    return (mask_t)(key & mask);
}

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

代碼邏輯還是很好理解的怜瞒,其查找循環(huán)邏輯和之前逆向的邏輯是等效的,不一樣的是循環(huán)退出的邏輯般哼,但兩者本來功能就不一樣吴汪。兩相印證,可以確認逆向代碼應該是正確的逝她。

接下來聊聊在類的方法列表中查找方法實現(xiàn)浇坐。

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

    runtimeLock.assertUnlocked();

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

    // Try this class's cache.
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

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

    // Try superclass caches and method lists.
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // 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.
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }

    // No implementation found. Try method resolver once.
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}
  1. 這里將根據(jù)cache開關,覺得是否查找緩存中的實現(xiàn)黔宛。但下面卻有直接查找的調(diào)用,這可能是蘋果的一個小失誤擒贸,但不會有什么副作用臀晃,也不會有bug。其中cache_getImp是匯編實現(xiàn)的介劫,直接使用了之前objc_msgSend中的CacheLookup宏徽惋,只不過參數(shù)是GETIMP,所以其只查找imp不調(diào)用座韵,找不到也沒有關系险绘。
  2. 調(diào)用getMethodNoSuper_nolock踢京,顧名思義,在當前類的查找該方法宦棺。這里需要說明的是瓣距,類中的方法列表是一個二維數(shù)組,其中第一維存著各Category方法列表或Class方法列表的指針代咸,第二維才是具體的方法列表蹈丸。其中Class方法列表的指針只有1個或0個。如果找到對應的方法就加載到緩存中呐芥。
  3. 如果前面都沒找到逻杖,那么就進入循環(huán)依次去父類查找。首先查找父類的緩存思瘟,如果找到并檢查是否是_objc_msgForward_impcachemessage轉(zhuǎn)發(fā)IMP荸百,因為后面邏輯顯示,該方法實現(xiàn)也會被加載到緩存中滨攻。如果不是管搪,則表明找到了對應的方法,記錄到緩存铡买,否則就退出循環(huán)更鲁。如果緩存沒有則跟2中一樣在該類的方法列表中查找。
  4. 如果沒有最終都沒有找到IMP奇钞,則調(diào)用_class_resolveMethod看能否響應該消息澡为。
  5. 如果第四步都沒有響應,則返回_objc_msgForward_impcache景埃,并記錄緩存媒至。

__objc_msgForward_impcache由匯編實現(xiàn),其代碼如下

    STATIC_ENTRY __objc_msgForward_impcache

    MESSENGER_START
    nop
    MESSENGER_END_SLOW

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr x17, [x17, __objc_forward_handler@PAGEOFF]
    br  x17
    
    END_ENTRY __objc_msgForward

其調(diào)用了__objc_forward_handler()谷徙,查源碼可知void *_objc_forward_handler = (void*)objc_defaultForwardHandler;拒啰,而這個默認的實現(xiàn)內(nèi)部沒有任何實質(zhì)性的功能。但有以下代碼可以在其他地方可以調(diào)用該函數(shù)該改變這個默認的實現(xiàn)完慧,

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

可搜索runtime源碼并無調(diào)用痕跡谋旦,線索在此就斷掉了。

不過我們可以下個斷點屈尼,看被誰調(diào)用了册着。

setForwardHandler.png
__CFInitialize.png

我們發(fā)現(xiàn)其在dyld加載image時被ImageLoaderMachO::doImageInit調(diào)用了,到dyld的源碼查找該函數(shù)脾歧,發(fā)現(xiàn)其循環(huán)調(diào)用了Image下注冊所有load_command對應的Initializer函數(shù)甲捏。也就是說__CFInitialize是由其他Image文件提供的。我們知道CF是CoreFaundation簡寫鞭执,我們到CoreFaundation的源碼中搜索發(fā)現(xiàn)確實有__CFInitialize司顿,但是卻沒有對objc_setForwardHandler調(diào)用芒粹,全局搜索也沒有。

不過在上圖斷點的調(diào)用中我們發(fā)現(xiàn)objc_setForwardHandler有getenv_CFStringGetUserDefaultEncoding大溜,而__CFInitialize源碼中確實也有這兩句化漆,應該是蘋果在開放CoreFaundation的時候由于某些原因刪除了相關的代碼×蕴幔可以通過Mac下的系統(tǒng)的CoreFoundation庫查找__forwarding__ 實現(xiàn)體(注意不是.tbd获三,tbd只包含描述,不包括實質(zhì)內(nèi)容锨苏,模擬器的dylib文件Mac下找疙教。iOS就麻煩點,有越獄機就容易了伞租,可惜我手上沒有越獄手機)贞谓,通過ida就很容易發(fā)現(xiàn)有該函數(shù)實現(xiàn)體,不過在自動逆向的時候出了問題葵诈。

我嘗試人肉逆向該函數(shù)裸弦,如果僅僅只是需要了解大致轉(zhuǎn)發(fā)邏輯流程,相對容易作喘,而且已經(jīng)有人做了(參考鏈接Hmmm, What's that Selector? )理疙,我和匯編代碼對照了一下,基本上是正確的泞坦,但很多細節(jié)被拋棄了窖贤,當然主要是這些細節(jié)破解確實比較麻煩,難以了解其背后C代碼的邏輯意義贰锁。目前我嘗試在破解這些細節(jié)赃梧,但結(jié)果不是特別滿意,所以也就沒有貼逆向的代碼豌熄,如果之后有比較好的進展再給出源碼授嘀。

總結(jié)

雖然分析說明的過程比較復雜,但是消息處理流程比較容易理解的锣险。objc_msgSend匯編部分僅僅完成很少的緩存查找功能蹄皱,如果找不到就會調(diào)用C方法去對象的方法二維數(shù)組中找,找不到再查父類的緩存(這也是匯編實現(xiàn)的)和父類的方法數(shù)組囱持,一直找到根類夯接,如果此過程中找到對應的方法則調(diào)用并添加緩存,如果沒有找到纷妆,則表明該繼承體系都沒有直接實現(xiàn)該方法,這時runtime會調(diào)用對象的方法決議去嘗試解決晴弃。如果不行則由CoreFoundation框架提供的__forwarding__來轉(zhuǎn)發(fā)到其他對象處理掩幢,若還不能處理則拋出異常逊拍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市际邻,隨后出現(xiàn)的幾起案子芯丧,更是在濱河造成了極大的恐慌,老刑警劉巖世曾,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缨恒,死亡現(xiàn)場離奇詭異,居然都是意外死亡轮听,警方通過查閱死者的電腦和手機骗露,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來血巍,“玉大人萧锉,你說我怎么就攤上這事∈龉眩” “怎么了柿隙?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鲫凶。 經(jīng)常有香客問我禀崖,道長,這世上最難降的妖魔是什么螟炫? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任波附,我火速辦了婚禮,結(jié)果婚禮上不恭,老公的妹妹穿的比我還像新娘叶雹。我一直安慰自己,他們只是感情好换吧,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布折晦。 她就那樣靜靜地躺著,像睡著了一般沾瓦。 火紅的嫁衣襯著肌膚如雪满着。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天贯莺,我揣著相機與錄音风喇,去河邊找鬼。 笑死缕探,一個胖子當著我的面吹牛魂莫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播爹耗,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼耙考,長吁一口氣:“原來是場噩夢啊……” “哼谜喊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起倦始,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤斗遏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鞋邑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诵次,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年枚碗,在試婚紗的時候發(fā)現(xiàn)自己被綠了逾一。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡视译,死狀恐怖嬉荆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酷含,我是刑警寧澤鄙早,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站椅亚,受9級特大地震影響限番,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呀舔,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一弥虐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧媚赖,春花似錦霜瘪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至磨隘,卻和暖如春缤底,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背番捂。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工个唧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人设预。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓徙歼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鲁沥,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉呼股,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評論 0 9
  • 動手實現(xiàn) objc_msgSend objc_msgSend 函數(shù)支撐了我們使用 Objective-C 實現(xiàn)的一...
    大鵬你我他閱讀 964評論 0 2
  • 概覽 每個Objective-C對象都有相應的類耕魄,這個類都有一個方法列表画恰。類中的每個方法都有一個選擇子、一個指向方...
    alvin_wang閱讀 754評論 0 0
  • 背起行囊 插一面中國勝了的小旗 我們走吧 無論前面是啥樣的 風雨 遠離喧囂 遠離這周圍的墻壁 我們走吧 別背那莫名...
    本無痕閱讀 366評論 12 12
  • 親愛的戰(zhàn)友鳳超兄:你好吸奴! 我是瀟瀟允扇,這是一封來自遠方問候的一封信。此刻的我現(xiàn)在湖北隨州则奥,見一個十幾年沒見面的一個好...
    Maggielxx閱讀 460評論 0 3