objc_msgSend 流程之缓存查找

前言

有一定经验的iOS开发者大家都知道OC方法调用的本质就是消息的发送,那么发送消息后底层到底是如何查找到消息的呢?今天我们结合源码分析一下(本次探究源码基于objc781).

注:本文会有少许的汇编代码知识,不熟悉汇编的同学可以自行补充一下简单的汇编知识。

为什么要方法缓存?

通过前面文章cache_t分析的分析,我们知道,当我们的 OC项目在编译完成之后,类的实例方法(方法编号 SEL 和函数指针地址 IMP)会保存在类的cache_t的方法列表中,那么为什么要方法缓存,直接每次查找不好吗?

原来如果我们每次都要去类的方法列表或者父类、根类的方法列表里面去查询函数地址的话,必然会对性能造成极大的损耗,所以OC 为了实现其动态性,将 方法的调用包装成了SEL 寻找 IMP 的过程。

准备工作

依旧是老样子我们定义一个ZGPerson类,代码如下:

@interface ZGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
@end

@implementation ZGPerson
- (void)sayHello{
    NSLog(@"%s",__func__);
}
@end

main.m中调用一下sayHello方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        ZGPerson *person = [ZGPerson alloc];
        [person sayHello];
    
    }
    return 0;
}

我们进入main.m文件路径执行clang命令clang -rewrite-objc main.m -o main.cpp,将main.m编译成c++文件并查看,发现编译后的代码如下

#pragma clang assume_nonnull end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 


        ZGPerson *person = ((ZGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

    }
    return 0;
}

这里我们可以看到调用方法的本质就是通过objc_msgSend方法给类发消息。

objc_msgSend方法分析

我们查看objc_msgSend的源码

/** 
 * Sends a message with a simple return value to an instance of a class.
 * 
 * @param self A pointer to the instance of the class that is to receive the message.
 * @param op The selector of the method that handles the message.
 * @param ... 
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method.
 * 
 * @note When it encounters a method call, the compiler generates a call to one of the
 *  functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
 *  Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; 
 *  other messages are sent using \c objc_msgSend. Methods that have data structures as return values
 *  are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
 */
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

下面上面的一长串简单的进行解释,当遇到一个方法调用时,编译器会根据调用类型生成一个底层函数:

  • 调用父类方法生成objc_msgSendSuper() 函数
  • 非父类方法生成objc_msgSend()函数
  • 如果返回值是数据结构,则使用 objc_msgSendSuper_stret或者 objc_msgSend_stret

objc_msgSend()是一个有两个默认参数id类型的selfSEL类型的_cmd (op),其中 self指向 消息接收者, _cmd方法选择器。如果需要传入更多的参数,可以拼接在这两个参数的后面。

我们在想进入源码中找objc_msgSend()的实现,发现已经点不进去了,我们在源码中搜索objc_msgSend(),发现在objc-msg-arm64.s中找到了objc_msgSend()汇编实现。

那么objc_msgSend()为什么用汇编实现呢?主要有一下两点原因:

  • 汇编更容易能被机器识别,效率更高(效率很重要)
  • C语言或者C++不能通过一个函数保留未知的参数并跳转任一未知的指针,而汇编可以

objc_msgSend汇编代码分析

我们以arm64架构的汇编代码为例进行分析
首先分析_objc_msgSendENTRY(入口)代码

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    //p0代表传入的对象,cmp比较,这里是判断传入的对象是否为空
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS     //是否是`tagged pointer`对象判断
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero    //直接return 0
#endif
    //获取传入对象的isa存入p13
    ldr p13, [x0]       // p13 = isa
    //获取传入对象的class类型存入p16
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // 调用方法寻找imp或者objc_msgSend_uncached
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

这里我在里面加了一些注释,这里主要对为什么获取isa进行下解释,是因为不管是对象方法还是类方法,我们都需要通过 isa 的指向 在类或元类的缓存或方法列表中去查找
下面是GetClassFromIsa_p16获取class的代码

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // 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__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

这里我们是arm64,直接通过and p16, $0, #ISA_MASK,即isa&mask得到类或元类 的信息

然后是调用方法CacheLookup寻找imp或者objc_msgSend_uncached,通过名字我们就可以猜出是去缓存中查找方法。

CacheLookup源码

.macro CacheLookup

LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    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


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

    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

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    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

这里的指令我们逐条解释:
ldr p11, [x16, #CACHE] // p11 = mask|buckets

注:
#define CACHE (2 * SIZEOF_POINTER)
#define CLASS SIZEOF_POINTER
x16 是我们上一步中获取到的 类信息x16偏移 16字节 就是取到cache_t 结构,存入 p11 中。

and p10, p11, #0x0000ffffffffffff // p10 = buckets

注:
我们首先解释一下_maskAndBuckets的结构,存储情况如下图

_maskAndBuckets

#0x0000ffffffffffff的二进制

可以看出 将p11#0x0000ffffffffffff进行与运算得到_maskAndBuckets0-47位即buckets,赋值给p10

and p12, p1, p11, LSR #48 // x12 = _cmd & mask

注:
LSP表示逻辑右移,将p11(_maskAndBuckets)右移48位得到_maskAndBucketsmask信息,然后与p1(_cmd)进行运算,赋值给p12
这里与我们上篇文章存方法的hash算法是一致的,目的是为了找到_cmdhash下标

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

注:
#define PTRSHIFT 3 // 1< LSL表示逻辑左移p10buckets数组的首地址,将_cmd&mask的结果左移4位,即向前偏移_cmd&mask位,假如_cmd&mask=3,即向前移3位,为什么是左移4位?因为一个bucket中包涵一个imp和一个sel,刚好16个字节,即左移4位。最后的结果(初始的bucket值)赋值给p12

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

注:
ldp指令ldr/str的衍生, 可以同时读/写两个寄存器, ldr/str只能读写一个
p12selimp ,分别存入p9p17

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

注:
我们将上一步获取到的 sel 和我们要查找的 _cmd(进行比较,如果匹配了,就通过 CacheHitimp返回;如果没有匹配,跳转到2

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

注:
CheckMiss $0 如果从最后一个元素往前遍历都找不到缓存,那么走 CheckMiss方法
cmp p12, p10 判断当前查询的 bucket 是否为第一个元素,如果相等,跳转到3,否则--bucket继续向前遍历

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    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)

注:
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))buckets首地址+mask右移44位,直接定位到buckets的最后一个元素,然后继续向前查找,进行递归循环

在第二次查找时,会重复上面的步骤,只有在最后一步有所不同

3:  // double wrap
    JumpMiss $0

如果第二次查找,查找不到的话就JumpMiss $0
JumpMiss源码

.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

CheckMiss源码

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

可见CheckMissJumpMiss差不多,此时的$0normal,会直接跳转至__objc_msgSend_uncached,即进入慢速查找流程

总结

最后的流程总结如下图


objc_msgSend缓存查找

好了,以上便是本篇文章的全部内容,如有不当之处,还望指正!慢速查找流程,我们后续会接着分析。

你可能感兴趣的:(objc_msgSend 流程之缓存查找)