objc_msgSend 源码阅读

objc_msgSend是OC中调用最为频繁的方法,所有OC方法的调用都离不开这个它。苹果已经将其开源(https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/),这是使用汇编语言编写的,其好处就是能提升函数的执行速度。本文选用它的arm64为汇编代码(objc-msg-arm64.s)进行分析。

函数入口

首先,找到ENTRY _objc_msgSend这一行,它是objc_msgSend的函数入口,下面逐行进行分析:

cmp p0, #0   将传入的第一个参数与0判断

这里的p0实际上就是x0,其定义在arm64-asm.h里面。

    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)

如果p0<0(即最高位为1),该对象是tagged pointer,实际上是一个为了节省空间而使用的特殊指针,关于它的详细描述可以看这篇文章,而当p0为0的时候,即代表传入对象为nil,函数应该立即返回,总之,都先要跳到LNilOrTagged进行特殊处理。。

如果p0>0,则代码会继续执行下去:

    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class

然后将x0指向内存中的值(isa)赋值给p13,然后通过GetClassFromIsa_p16的宏后,p16得到了class的地址。GetClassFromIsa_p16的实现如下(剔除了SUPPORT_INDEXED_ISA的部分,因为它是针对watch的):

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

ISA_MASK是定义在isa.h的宏,其值为0x0000000ffffffff8ULL。

可以看出class的地址是isa指针跟ISA_MASK与运算得来的,其中的关系可以参考这篇文章,这里就不展开讲了。

LGetIsaDone:
    CacheLookup NORMAL  

接下来,就是查缓存的流程,在讲这个之前,先把其它分支条件过一遍。

LNilOrTagged

LNilOrTagged的实现723版本和750版本的不太一样,不过原理是一样的,先看下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返回。接下来就是处理tagged pointer的逻辑。
  2. tagged pointer有两种,一种是系统的,其isa的前4位为标志位,最高位位1。另一种是开发者扩展的,其isa的前8位是标志位,前4位都是1。因此,如果p0比0xf00....要大(这里是无符号比较),就跳到LExtTag进行扩展的处理,否则执行系统tagged pointer的逻辑。
  3. 取出_objc_debug_taggedpointer_classes的地址加载到x10中
  4. 获取x0的高4位保存到x11中(高4位也是isa指针在_objc_debug_taggedpointer_classes中的索引)
  5. 以x11作为索引,算出对应isa指针的内存地址存到x16中
  6. 取出class地址之后执行LGetIsaDone

对于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的版本并没有区分tagged pointer是系统的还是扩展的,直接就将其当成系统的处理取出class地址,在这之后,又将___NSUnrecognizedTaggedPointer的地址赋给x10,如果取出的这个class地址跟NSUnrecognizedTaggedPointer相等,就代表这是一个扩展指针(因为如果是扩展指针的话,最高4位必须是1,通过前面的运算之后x16存的地址只能是一个确定的值。也可以由此推断出___NSUnrecognizedTaggedPointer_objc_debug_taggedpointer_classes中的索引是0x1111。

我看不出750的实现方式优越在哪个地方,看起来都是9条汇编代码,希望有大神来解释一下。

LExtTag

求扩展的tagged pointer的class地址和系统的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. 通过索引求出class的地址并将其放到x16
  4. 执行LGetIsaDone

LReturnZero

如果p0=0,则说明传入的类为nil,这个时候应该执行返回nil的逻辑

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

在arm64位中,函数整型的返回值会存在x0,x1中,而浮点数的返回值存在v1-v3中,由于不知道函数的调用者需要什么类型,因此会将上述寄存器都清空,x0已经是0了,因此不需要清空。

CacheLookup

不管是哪个分支条件,来到CacheLookup这个宏之后,p16都已经得到类的地址了,接下来就是查找缓存的过程

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

将class地址的CACHE偏移量的内存赋值给p10和p11,对于CACHE的定义可以在本文件中找到:

/* Selected field offsets in class structure */
#define SUPERCLASS       __SIZEOF_POINTER__
#define CACHE            (2 * __SIZEOF_POINTER__)

其中SIZEOF_POINTER是8个字节,因此这里偏移了16个字节,在objc-runtime-new.h中,我们可以找到objc_class的实现如下(注意不是runtime.h里面的objc_class,后者已经废弃掉了):

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
    ...

对于其父结构体objc_object,其定义在objc.h

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

这样,我们可以推断出isa指针的偏移量是0,superClass的偏移量是8,cache的偏移量是16。

对于cache_t的结构体,定义如下:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

其中bucket_t占了8字节,而mark_t占了4个字节,因此ldr p10, p11, [x16, #CACHE]的结果是p10存了_buckets,p11高32位存_occupied,低32位存_mask(因为arm64默认是小端)
_buckets就是存缓存函数地址的地方,实际上是一个哈希表,_mask总是2的n次幂-1,也就是0x00....1111,通过它和函数方法可以求出函数在哈希表中的索引。

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

通过上面的运算,就可以得到函数方法在哈希表的索引,实际上就相当于_cmd%哈希表的大小,可以看出,也就是说哈希表的构造方法是除留余数法。

    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

PTRSHIFT 的定义在arm64-asm.h,也就是3。p10是_buckets的首地址,因为bucket_t的大小为16个字节,所以需要将索引乘以16,也就是左移3位。计算完之后,p12里面就是对应的bucket的指针了。

    ldp p17, p9, [x12]      // {imp, sel} = *bucket

将bucket加载到p17和p9,bucket_t的结构如下:

struct bucket_t {
    MethodCacheIMP _imp;
    cache_key_t _key;
    ...

通过这一运算,p17存放了_imp,p9存放了_key,而_key实际上就是sel。

1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

找到sel后,就让传进来的sel和找到的sel作对比,如果一样,则跳到CacheHit执行函数,如果没找到,可能是出现哈希冲突了,开始继续查找找的逻辑。

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的作用是找出来的sel是否为nil,是的话就跳出汇编用C语言的方式找,其实现一会再讲,如果p12和p10相等,即找到的bucket是buckets的首地址,那就跳到3(跳到最后一个bucket继续查找)如果不是,则倒序查找跳到前一个bucket,跳回1继续查找。

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.

能来到3,说明找到的bucket是buckets的第一个,这个时候,跳到最后一个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

接下来执行的1,2跟上面的1,2一样,不一样的是,如果再次碰到第一个bucket,就跳出汇编。

CacheHit的宏如下:

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

对于TailCallCachedImp,它定义在arm64-asm.h

.macro TailCallCachedImp
    // $0 = cached imp, $1 = address of cached imp
    brab    $0, $1
.endmacro

这个时候,x12存了IMP的地址,x17存了保存的IMP,但是brab是什么命令我没查到,大意应该就是调用了这个缓存的函数。

总结一下CacheLookup这个流程,如果缓存高级语言的写法,那应该就是:

bucket_t bucket = class->cache->buctet[sel]
if (sel == bucket->_key){
    bucket]->_imp()
} else{
    //执行C语言的逻辑
}

缓存找不到的case

不管是JumpMiss还是CheckMiss的时候sel为空(也就是没有找到缓存的sel)最后都会来到
_class_lookupMethodAndLoadCache3这个C函数这个函数定义在objc-runtime-new.mm,代码如下:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/); 
}

这个方法返回查找过后IMP指针,供汇编代码调用,这里就不贴出返回之后的逻辑了。而lookUpImpOrForward是一个查找方法的函数

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

    runtimeLock.assertUnlocked();
    
    //因为传入的cache为NO,所以不会执行(汇编已经执行过一遍了)
    if (cache) { 
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.lock();
    
    //检查类是否是已知的类,如果是NSClassFromString()方法得到的,那有可能是未知的
    checkIsKnownClass(cls);

    // 判断类是否已经实现,如果没有先将其实现
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    //检查类是否被初始化,如果没有,则将其初始化
    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) {
                    //判断这是不是消息转发的方法
                if (imp != (IMP)_objc_msgForward_impcache) {
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    //如果是消息转发,先不调用
                    //先调用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;
            }
        }
    }

    // 如果没找到实现,则调用+ (BOOL)resolveClassMethod:(SEL)sel
    //和+ (BOOL)resolveInstanceMethod:(SEL)sel方法,重新试一次

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }
    
    //如果还是找不到,走消息转发
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

这个方法其实就是在类中查找方法的整套流程实现。这个过程是线程安全的。找到的方法都会调用cache_fill存到缓存里面。一旦方法被缓存起来,下次调用的时候则只需要执行汇编的代码就可以找到方法。大大地提高代码执行的效率。

参考文献

  • 逐行剖析objc_msgSend汇编源码
  • iOS Tagged Pointer
  • Objective-C 中的消息与消息转发

你可能感兴趣的:(objc_msgSend 源码阅读)