解读objc_msgSend

引入

众所周知,Objective-C动态性的根源在方法的调用是通过message来实现的,一次发生message的过程就是一次方法的调用过程。发送message只需要指定对象和SEL,Runtime的objc_msgSend会根据在信息在对象isa指针指向的Class中寻找该SEL对应的IMP,从而完成方法的调用。这样每次方法的调用必然会有方法的查找过程,如果频繁调用,或者Class的方法列表过大,很容易导致性能瓶颈,但OC似乎并没有这个问题,这得益于苹果的优化机制,其中包括纯汇编的objc_msgSend实现(不用汇编参数暂存困难,当然也不是没有办法,总有一些歪招可以解决,但为了兼顾效率自然汇编更加合适),方法查找cache,TaggedPointer等等技术才带来OC极高的效率。接下来我们从objc_msgSend为引,来解读整个过程。

Objective-C动态化的核心objc_msgSend

先说几句闲话,如果大学期间学习过汇编课程的同学就知道,相同的逻辑,c语言写出的函数汇编成.s文件和直接汇编的的文件,体积差异是很大的,几倍到几十倍的差距,由此可见两者的效率差距也十分巨大,这也是苹果为什么非要用汇编去干这个事情。而且苹果汇编只实现了cache方法的查找过程,并未汇编实现class所有的方法查找,因为前者调用非常频繁,后者却不是,其在运行效率和开发效率(可靠性)作了一个平衡。

进入正题,我们知道对于任意的OC方法的调用,比如[obj aMethod];都会被翻译成objc_msgSend(obj, sel/*@selector(aMethod)*/);,由此进入objc_msgSend执行,而该方法实现是汇编完成的,这对解读造成了一定的困扰,所以我们不得不迎难而上,搞定整个方法的逻辑过程。

我这里下载的objc_706的源码,这里我只解读objc-msg-arm64.s文件,其他处理器架构的除了实现细节出入,其查找逻辑的类似。

文件的最开始声明了12个私有的_objc_entryPoints,其中包括我们关注的.quad _objc_msgSend

在文件中搜索"_objc_msgSend",会找到以下汇编代码,这就是其实现的一部分,接下来我们将一步步解读它。


    .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
数据结构定义

咋一看,只能了解其定义了一些数据存储的空间,里面存储的应该是指针,而且这些指针3bit对齐,似乎跟OC的objc_object指针很类似,然后通过.fill伪指令,将所有的数据单元填0。至于这些数据怎么使用我们并不了解,没关系,我们继续往下看。

找到了ENTRY _objc_msgSend,顾名思义这就是真正的函数入口了。我们搜索ENTRY,其定义如下:

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

这里定义了一个汇编宏,表示text段,定义一个global的_objc_msgSend,"$0"同时生产一个函数入口标签。

UNWIND _objc_msgSend, NoFrame则定义了一些段存储数据对象,简单来说就是类似于结构体数据对象,具体意义我也不是很了解。

MESSENGER_START定义了一些方法调用开始的调试数据,具体对应到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;
};

函数逻辑主体

逻辑部分有很多子逻辑,我们一步一步解读

tagged pointer处理

cmp x0, #0,从注释可以了解是在和"0"比较,比较的结果会有三种,大于小于等于。

  1. 这里逻辑是b.le LNilOrTagged,即如果小于等于就跳转到标签:LNilOrTagged。(因为nil==0,tagged指针最高位是1(符号位),所以肯定小于0)。

  2. 跳转到LNilOrTagged后,执行b.eq LReturnZero继续检查比较结果,相等则跳转标签:LReturnZero

  3. 在LReturnZero中,将x1置为0,浮点寄存器d1,d2,d3,d4全部置为0。这是因为objc_msgSend并不知道,该函数调用期望返回的是什么数据类型,可能是浮点,整型,指针,甚至可能结构体,所以其将常用的返回值的寄存器,全部清0。但对于复杂的结构体,objc_msgSend就无能为力了(因为其不知道这些数据的大小),它只能将返回结果放入x8寄存器,由另外代码去清0,而这部分代码则编译器在编译的时候根据相关数据类型生成。

  4. 如果b.eq LReturnZero不成立,则表明该数据是个tagged pointer,需要进一步处理才能做调用。

    mov x10, #0xf000000000000000; cmp x0, x10,这两句就很明显了。比较其最高的4bit,这应该是个标记位。我们去搜索其相关的定义

    
    #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(),发现tagged指针相关的操作这里都有,其中_objc_makeTaggedPointer的功能是实现原始数据封装成tagged指针。

tagged_pointer.png

这里我们画了一个简图,是arm64下的存储结构(其他CPU下并不一样,比如X86_64,data存在前部,tag存后部),如果是个扩展的tagged指针,其中0-51位是数据部分,52-59这8个bit是扩展tagged部分,60到62的3bit是tagged,63是tagged指针标记。如果是一个tagged指针0-59是数据,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文件中有以下声明,所以这两个数据是汇编语言定义,但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,如果比较结果是大于等于,则表示这是个扩展的tagged,跳转到标签:LExtTag

  1. 接来两句是加载_objc_debug_taggedpointer_ext_classes

. ubfx x11, x0, #52, #8 的意思是x11= (x0 & 0x0ff0000000000000)>>52,即取第52-59bit的数据。

. ldr x16, [x10, x11, LSL #3],x16=x10+(x11<<3),左移三位是因为_objc_debug_taggedpointer_ext_classes是8个byte为单位来偏移的,后面做汇编逆向源码的时候会有类似的例子来说明。

  1. 如果第五步不成立,则执行按照正常的tagged指针处理,加载_objc_debug_taggedpointer_classes,取出第60-63bit,左移三位,取出真正的isa。

  2. 跳转LGetIsaDone,其使用了汇编宏CacheLookup,参数NORMAL,其用于搜索缓存。

到这里,汇编的第一部分就解读完成了,其主要就是解析当前指针得到对应的class以备后续处理。为了更好的理解,这里我贴出逆向出来的源码,以便于理解。

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代码,所以很容易了解其大概做了什么逻辑,但要看懂具体细节,并逆向出源码,对于不常玩汇编的人来说,还是有那么一点困难。

  1. 之前操作,已经在x1和x16中存入处理好的相关数据,x0=obj,x1=SEL,x16=isa。立即数#CACHE=16,OC对象的内存布局中,前面分别是isa和superclass指针,x16+16就是cache的地址。cache结构体数据布局如下。

    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的数据到x9和x17。

  2. cmp x9, x1,比较x9与x1,也就是bucket->_key与SEL。

  3. b.ne 2f如果不相等跳转到标签2

  4. 接上面一步,如果相等则CacheHit $0,表示命中缓存,CacheHit是一个宏,“$0”是第一个参数,就是之前的NORMAL。这是将执行br x17,跳转到寄存器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. 如果第一次没有找到该缓存那么就调用宏CheckMiss $0,也就是执行cbz x9, __objc_msgSend_uncached,嘛意思呢,就是x9和0比较,如果相等则跳转__objc_msgSend_uncached,其内部实现主要是调用 __class_lookupMethodAndLoadCache3,这个是c代码实现的,后面再说。

  6. cmp x12, x10,这里是比较cache.buckets()和当前指向的bucket比较,看是否是一样。

  7. b.eq 3f 相等跳转到标签3,否则顺序执行下一指令。

  8. ldp x9, x17, [x12, #-16]!就是将bucket自减,取下一条数据,跳转到标签1循环执行

  9. 接步骤9,x12=x12+w11<<4,并进入下一部分代码。可以发现与之前的代码几乎一致,除了末尾的JumpMiss,也就是出口。

虽然解读了代码,但具体是在做什么逻辑,可能还是不太明白,需要说明一下。

首先_buckets是一个简单的hash表,就是数据结构课上讲的那种最基本hash表,hash值计算公式就是最简单的hash=sel地址%mask,其中mask就是存储空间的大小,初始大小是4,如果不够用时(使用空间大于总空间的3/4)则增长一倍。根据sel地址计算出的hash值作为偏移量存储IMP。

有了这个基础,再回顾上面的代码逻辑。

如果当前sel的地址与存储的bucket->sel一样,那就表示已经有缓存了,直接调用即可。否则检查bucket->sel是否为0,如果为0则表明肯定还没有建立缓存,则直接调用c代码建立缓存。如果不等于0,则表示此处被其他的sel占用了,这时候就需要通过逐项搜索检查是否已经缓存(因为已计算了index,所以搜索距离会大幅减少),同时检查bucket是不是已经移动到最开始,如果不是则移动指针查找下一个bucket,否则将bucket直接跳转到最末尾继续查找。还是画个图吧,这样就清晰了。
需要注意的是这里用了一个小技巧,bucket的查找是反向的,这样可以不需要知道bucket具体大小,就可以判断是否已经查找完前部,然后跳转到后部。

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) {
            //调用汇编方法__objc_msgSend_uncached();
            //其直接调用了c方法__class_lookupMethodAndLoadCache3
        }
        
    } while((cache.buckets() == bucket) ?
            bucket = &(cache.buckets()[cache.mask()])
            : --bucket);
    
    return nil;
}

可以看出objc_msgSend只用汇编写了很少的代码,只包含tagged指针处理和方法缓存查找,但是其带来的效率提高却是巨大的,非常符合28原则,80%情况下调用了20%代码,苹果就是在这20%的代码上尽可能的提高效率,带来明显的收益。

缓存的建立

以上就是是缓存的查找逻辑,那么究竟是否正确,我们需要找到缓存的建立逻辑相互印证,才能得出结论。

进入之前说到的__objc_msgSend_uncached,其就两句MethodTableLookup; br x17,而前一句里面则直接跳转bl __class_lookupMethodAndLoadCache3,其缓存加载的主线调用逻辑如下(其他逻辑暂时先不关注)

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. 调用cache->find查找缓存,如果没有找到,则添加新缓存,调用incrementOccupied将occupied++;
  3. 只要调用了本函数,不管有没有找到,都把原缓存覆盖掉。

那么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;
}

代码逻辑还是很好理解的,其查找循环逻辑和之前逆向的逻辑是等效的,不一样的是循环退出的逻辑,但两者本来功能就不一样。两相印证,可以确认逆向代码应该是正确的。

接下来聊聊在类的方法列表中查找方法实现。

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. 这里将根据cache开关,觉得是否查找缓存中的实现。但下面却有直接查找的调用,这可能是苹果的一个小失误,但不会有什么副作用,也不会有bug。其中cache_getImp是汇编实现的,直接使用了之前objc_msgSend中的CacheLookup宏,只不过参数是GETIMP,所以其只查找imp不调用,找不到也没有关系。
  2. 调用getMethodNoSuper_nolock,顾名思义,在当前类的查找该方法。这里需要说明的是,类中的方法列表是一个二维数组,其中第一维存着各Category方法列表或Class方法列表的指针,第二维才是具体的方法列表。其中Class方法列表的指针只有1个或0个。如果找到对应的方法就加载到缓存中。
  3. 如果前面都没找到,那么就进入循环依次去父类查找。首先查找父类的缓存,如果找到并检查是否是_objc_msgForward_impcachemessage转发IMP,因为后面逻辑显示,该方法实现也会被加载到缓存中。如果不是,则表明找到了对应的方法,记录到缓存,否则就退出循环。如果缓存没有则跟2中一样在该类的方法列表中查找。
  4. 如果没有最终都没有找到IMP,则调用_class_resolveMethod看能否响应该消息。
  5. 如果第四步都没有响应,则返回_objc_msgForward_impcache,并记录缓存。

__objc_msgForward_impcache由汇编实现,其代码如下

    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

其调用了__objc_forward_handler(),查源码可知void *_objc_forward_handler = (void*)objc_defaultForwardHandler;,而这个默认的实现内部没有任何实质性的功能。但有以下代码可以在其他地方可以调用该函数该改变这个默认的实现,

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

可搜索runtime源码并无调用痕迹,线索在此就断掉了。

不过我们可以下个断点,看被谁调用了。

setForwardHandler.png
__CFInitialize.png

我们发现其在dyld加载image时被ImageLoaderMachO::doImageInit调用了,到dyld的源码查找该函数,发现其循环调用了Image下注册所有load_command对应的Initializer函数。也就是说__CFInitialize是由其他Image文件提供的。我们知道CF是CoreFaundation简写,我们到CoreFaundation的源码中搜索发现确实有__CFInitialize,但是却没有对objc_setForwardHandler调用,全局搜索也没有。

不过在上图断点的调用中我们发现objc_setForwardHandler有getenv_CFStringGetUserDefaultEncoding,而__CFInitialize源码中确实也有这两句,应该是苹果在开放CoreFaundation的时候由于某些原因删除了相关的代码。可以通过Mac下的系统的CoreFoundation库查找__forwarding__ 实现体(注意不是.tbd,tbd只包含描述,不包括实质内容,模拟器的dylib文件Mac下找。iOS就麻烦点,有越狱机就容易了,可惜我手上没有越狱手机),通过ida就很容易发现有该函数实现体,不过在自动逆向的时候出了问题。

我尝试人肉逆向该函数,如果仅仅只是需要了解大致转发逻辑流程,相对容易,而且已经有人做了(参考链接Hmmm, What's that Selector? ),我和汇编代码对照了一下,基本上是正确的,但很多细节被抛弃了,当然主要是这些细节破解确实比较麻烦,难以了解其背后C代码的逻辑意义。目前我尝试在破解这些细节,但结果不是特别满意,所以也就没有贴逆向的代码,如果之后有比较好的进展再给出源码。

总结

虽然分析说明的过程比较复杂,但是消息处理流程比较容易理解的。objc_msgSend汇编部分仅仅完成很少的缓存查找功能,如果找不到就会调用C方法去对象的方法二维数组中找,找不到再查父类的缓存(这也是汇编实现的)和父类的方法数组,一直找到根类,如果此过程中找到对应的方法则调用并添加缓存,如果没有找到,则表明该继承体系都没有直接实现该方法,这时runtime会调用对象的方法决议去尝试解决。如果不行则由CoreFoundation框架提供的__forwarding__来转发到其他对象处理,若还不能处理则抛出异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,633评论 0 9
  • 动手实现 objc_msgSend objc_msgSend 函数支撑了我们使用 Objective-C 实现的一...
    大鹏你我他阅读 944评论 0 2
  • 概览 每个Objective-C对象都有相应的类,这个类都有一个方法列表。类中的每个方法都有一个选择子、一个指向方...
    alvin_wang阅读 724评论 0 0
  • 背起行囊 插一面中国胜了的小旗 我们走吧 无论前面是啥样的 风雨 远离喧嚣 远离这周围的墙壁 我们走吧 别背那莫名...
    本无痕阅读 358评论 12 12
  • 亲爱的战友凤超兄:你好! 我是潇潇,这是一封来自远方问候的一封信。此刻的我现在湖北随州,见一个十几年没见面的一个好...
    Maggielxx阅读 447评论 0 3