OC方法调用之objc_msgSend快速查找

1,379 阅读5分钟
int main(int argc, const char * argv[]) {

    @autoreleasepool {
        JPerson *person = [JPerson alloc];
        [person instanceMethod]; //打断点
    }
    return 0;
}

我们看到方法调用的底层的是objc_msgSend

image.png

objc_msgSend

OC底层的绝大部分方法调用是通过objc_msgSend消息发送机制实现的,少量方法是通过函数地址直接调用。

正是由于objc_msgSend执行频率非常高对于其执行效率的要求也相应提高,OC底层对于objc_msgSend的实现是直接使用汇编语言实现的。

我们只分析arm架构objc-msg-arm64.s文件,汇编中方法是以ENTRY开头的

本文基于源码objc4-818.2.tar.gz

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    // po是消息的接收者receiver,这里是判断消息的接收者是否为空
    cmp p0, #0 // nil check and tagged pointer check
    
    // 如果消息的接收者为空那么再判断是不是tagged_pointer对象
#if SUPPORT_TAGGED_POINTERS
    // tagged_point对象执行LNilOrTagged逻辑
    b.le LNilOrTagged //  (MSB tagged pointer looks negative)
#else
    // 否则走LReturnZero逻辑
    b.eq LReturnZero
#endif

    // 前面处理了方法的接收者不存在的情况,到这里就确保了方法的接收者是存在的
    // x0寄存器存储了指向实例对象person的指针,[x0]表示根据指针取值
    // 这里表示将person实力对象的前8字节取出存储到p13中
    // 实例对象的前8字节正是isa,所以 p13 = isa
    ldr p13, [x0] // p13 = isa
    
    // 根据注释我们知道这行代码执行完之后p16存储指向类对象的指针
    GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

GetClassFromIsa_p16

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA // 这里是iwatch代码,先不看
    // Indexed isa
    mov p16, \src // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
    // isa in p16 is indexed
    adrp x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
// 根据前面参数知道 needs_auth=1
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    mov p16, \src
.else
    // 64-bit packed isa
    // 所以最终执行了这里
    // 根据前面传入参数可知道 
    // src = p13 = isa 
    // auto_address=x0也就是对象指针
    ExtractISA p16, \src, \auth_address
.endif
#else
    // 32-bit raw isa
    mov p16, \src
#endif
.endmacro

ExtractISA

.macro ExtractISA
    // $0 = p16
    // $1 = isa
    // 这句代码意思是 p16 = isa & ISA_MASK
    and    $0, $1, #ISA_MASK
.endmacro

通过前文# OC底层之类结构探索我们知道isa & ISA_MASK可以得到指向类对象的指针,也就是p16 = class

CacheLookup

汇编语言阅读起来很吃力,我只是整理了一些主流程代码分析一下,我的目的不在于系统的学习汇编而在于学习cache中如何进行查找的流程

#define CACHE            (2 * __SIZEOF_POINTER__)

CACHE表示2个指针的大小

struct bucket_t {
private:
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
...
};

__arm64__架构下bucket_t中前8字节存储的是imp8字节存储的是sel,后面分析会用到

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

    // 根据前面分析知道x16存储的是指向类对象的指针
    // 这行代码的意思是读取x16偏移CACHE大小的内存,
    // CACHE的值为两个指针的大小,
    // 类对象中的isa和superclass刚好是两个指针
    // 那么我们就知道p11的值为cache的前8个字节
    // 也就是 _bucketsAndMaybeMask
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES

    // 通过前文的分析
    // buckets = _bucketsAndMaybeMask & preoptBucketsMask
    // p10存储的就是buckets的地址
    and p10, p11, #0x0000fffffffffffe // p10 = buckets
    
    // 这里判断p11是否为空
    tbnz p11, #0, LLookupPreopt\Function
    // 为空的逻辑就不看了。。。
    
    // 这里是p11不为空的逻辑,这行代码的意思是
    // p12 = p1 异或 (p1, LSR #7)  p1为方法的sel
    // p1, LSR #7 表示p1按位右移7位 得到的是一个mask
    // 等价于p12 = sel ^ mask,
    // 根据前文分析buckte中存储的sel就是通过sel^mask的值
    // 从而得到p12存储的就是sel在bucket中存储的值
    eor p12, p1, p1, LSR #7
    and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
    
    // p13的值为sel在buckets中的位置,这里简单理解就是一个哈希算法
    add p13, p10, p12, LSL #(1+PTRSHIFT)
                        // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
                        // do {
// 这行代码的意思是从x13指向的内存空间取出来16个字节的值
// 前8字节存储到p17中,后8字节存储到p9中,
// 然后x13=x13-BUCKET_SIZE,既x13指向前一个bucket的位置
// 从前文中得知arm架构下前8字节为imp,后8字节为sel
// 所以p17=_imp  p9=_sel,_sel & mask可以得出真正的sel
1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--

    // p1是参数传入sel p9是从bucket中取出来的sel
    // 比较是不是我们要找的sel
    cmp p9, p1 //     if (sel != _cmd) {
    
    // 如果不是就跳转到3,否则就继续执行2
    b.ne 3f //         scan more
                //     } else {
// CacheHit为命中的逻辑,既找到了缓存的bucket
2: CacheHit \Mode // hit:    call or return imp
                //     }
// 如果p9==0
3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;

    // p13为当前在buckets中指向的位置,
    // p10为buckets首地址
    // p13从哈希算法确定的位置往前找,判断是不是找到首地址的位置了
    cmp p13, p10 // } while (bucket >= buckets)
    
    // 如果还没到就跳转到1继续往前找
    b.hs 1b
    
    // 这里表示找到首地址的位置仍然没找到
    // 这里可以简单理解为p13指向buckets最后一个位置了
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                        // do {
// 这里还是取出_sel存储到p9中然后x13指向前一个bucket的位置
4: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--

    // 比较sel确定是不是我们要找的
    cmp p9, p1 //     if (sel == _cmd)
    
    // 如果是,就跳转到2执行命中的逻辑
    b.eq 2b //         goto hit
    
    // 如果不是判断是不是空bucket
    cmp p9, #0 // } while (sel != 0 &&
    
    // 这里判断是不是找到哈希算法第一次确定的位置了
    ccmp p13, p12, #0, ne //     bucket > first_probed)
    
    // 如果还没到,说明还没找完,跳转到4位置继续向前找
    b.hi 4b
    
// 否则的话就是没有缓存过,找不到
LLookupEnd\Function:
LLookupRecover\Function:
    b \MissLabelDynamic

.endmacro

简单来说就是根据方法的sel经过哈希算法确定buckets中的一个位置挨个向前查找,如果找不到就从最后一个向前找,如果找到最初的位置还没找到说明没缓存过,如果找到了就执行命中逻辑CacheHit

CacheHit

.macro CacheHit
.if $0 == NORMAL
    // 根据前面代码我们知道$0=NOMAL
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    ...
.endmacro

TailCallCachedImp

.macro TailCallCachedImp
// $0为从bucket中查到的_imp,
// 从前文得知 bucket中存储的_imp = imp ^ 类指针
// $3 = x16正是类指针,所以imp = _imp ^ $3
eor $0, $0, $3

// 执行imp
br $0
.endmacro

缓存的时候并不是直接将方法的imp存储到bucket中的,而是imp类地址经过按位异或的结果存储起来的,而按位异或是可逆的

  • a ^ mask = c => a = c ^ mask 所以这里其实就是还原真正的方法地址imp并执行之

小结

方法的快速查找之所以阅读不好理解很大程度上是因为他是用汇编实现的,但是当我们硬着头皮阅读一遍源码之后其实他的逻辑很简单,就是根据方法的sel经过哈希算法确定buckets中的一个位置,如果没命中就向前查找,如果找到buckets首地址还没有找到就从最后一个向前继续查找,如果找到哈希算法确定的位置还没有命中,那就是没缓存过。

如果命中了就还原imp并执行方法

参考文章

# objc_msgSend分析-快速查找