方法调用(一)-- objc_msgSend快速查找流程

方法调用(一)-- objc_msgSend快速查找流程
方法调用(二)-- 慢速查找流程
方法调用(三)-- 动态方法决议&消息转发


开场白

前一篇文章cache_t分析,对方法调用后,类中会对该方法进行缓存。
而完整的缓存流程,要先进行查找,简要流程图如下:

  • 缓存中不存在:进行缓存,这一步就是cache_t中‘保存’方法实现的内容。
  • 缓存中存在:就直接返回

上图只是简要的缓存流程,本文主要研究的方法调用时,查找过程中做了什么?

1.切入点 - objc_msgSend

定义DZStu类,类中有方法sayHello,并且进行了实现。相关代码如下:

@interface DZStu : NSObject
- (void)sayHello;
@end

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZStu *stu = [DZStu alloc];
        [stu sayHello];
    }
    return 0;
}

通过两种方式,可以知道底层调用的函数是什么。

1.1 汇编

通过在方法调用的位置下断点,也就是[stu sayHello];此行代码,运行后打开汇编

可以看到底层调用的是objc_msgSend函数

1.2 clang

使用clang命令

  • 进入文件所在的目录,示例代码写在main.m文件中,进入main.m文件的路径。
  • 执行命令clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk main.m
  • 会在同级目录中生成main.cpp文件。
  • 打开生成的文件,找到main函数

此处也可以看到底层调用的是objc_msgSend函数

1.3 用objc_msgSend函数模拟方法调用

我们可以直接使用objc_msgSend函数来调用sayHello方法:

#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DZStu *stu = [DZStu alloc];
        [stu sayHello];
        
        objc_msgSend(stu, sel_registerName("sayHello"));
        
    }
    return 0;
}

运行结果,如图:


此处需要注意:

  1. 需要引用文件objc/message.h
  2. 会有如图中的报错

报错信息是参数过多,此处需要修改xcode中配置,BuildSettings中搜索msg

将如图中的Enable Strict Checking of Objc_msgSend Calls设置为NO

2. objc_msgSend 快速查找流程

2.1 objc_msgSend

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend
    
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    ...//省略部分代码

#endif

LReturnZero:
    // x0 is already zero
    ...//省略部分代码

    END_ENTRY _objc_msgSend
  • 判断objc_msgSend第一个参数,也就是接受者是否有效,此处也进行nil或者tagged pointer类型的判断。获取到对象的isa
  • GetClassFromIsa_p16 p13,通过获取到的isa,进而获取到类。
  • 最终调用CacheLookup

下面分析GetClassFromIsa_p16这个函数

2.2 GetClassFromIsa_p16

.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
  • #if SUPPORT_INDEXED_ISA:这个值等于0,所以这个条件判断不会进入
  • #elif __LP64__:主要研究的方向是这个分支:
    • $0:传入的参数,就是对象的isa
    • #ISA_MASK:在64位设备上取值是0x0000000ffffffff8ULL
    • 通过操作,也就是通过isa的掩码获取到类信息。并存储到p16中。
  • #else:32位情况下的isa

2.3 CacheLookup

重点部分,真正的快速查找流程:

.macro CacheLookup

LLookupStart$1:

    // p1 = SEL, p16 = isa
    //1.宏CACHE表示两个指针的大小,p11中就是cache中的首地址,也就是mask|buckets
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //2.获取buckets,保存在p10中
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    
    //3.获取_cmd & mask,存在p12中,这个值是在buckets中开始查找位置的下标
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ......
#else
    ......
#endif

//4.调整p12指向循环的开始位置
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//5.获取当前的「imp,sel」
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    
    //6.查找的bucket的sel与_cmd比较,相等调用CacheHit;不相等时走2
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
    //7.当前的bucket == buckets数组首元素地址
    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
//8.遍历到第一个元素时,再次指向最后一个元素
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ......
#else
#error Unsupported cache mask storage for ARM64.
#endif

//9.同上,再次进行循环,此次是从最后一个元素开始,向前找
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

关键步骤解释:

  1. ldr p11, [x16, #CACHE]:x16中保存的就是isa,#CACHE是一个宏(#define CACHE (2 * __SIZEOF_POINTER__)表示两个指针大小),p11中保存就是mask|buckets(arm64环境中,mask和buckets存在一起)
  2. and p10, p11, #0x0000ffffffffffff:将buckets保存到p10
  3. and p12, p1, p11, LSR #48:将_cmd & mask保存到p12
  4. add p12, p10, p12, LSL #(1+PTRSHIFT)
    • PTRSHIFT是个宏,值为3,此步相当于p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    • buckets进行指针偏移,偏移的步长是1+PTRSHIFT = 4,偏移步数是:_cmd & mask
  5. ldp p17, p9, [x12]:获取当前定位到的bucket
  6. cmp p9, p1:判断当前的获取到sel与传入的参数p1(_cmd)是否相等
    • 相等:直接走CacheHit流程,从缓存中找到了
    • 不相等:进入循环
  7. 循环中的判断cmp p12, p10:此处是判断当前遍历到的bucket是否就是数组buckets的首地址。如果不是:向前查找
  8. 如果是:执行add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)),相当于p12 = buckets + (mask << 1+PTRSHIFT),buckets偏移到最后的元素(mask总个数。1+PTRSHIFT = 4,同上,偏移的步长)
  9. 再次进行循环,此次是从最后一个元素开始,同上面的循环一样,从后往前查找。

2.4 CheckMiss & JumpMiss

CheckMiss & JumpMiss源码:

.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

.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

⏬⏬⏬

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
    
MethodTableLookup   //在方法列表中查找
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

⏬⏬⏬

.macro MethodTableLookup
......//省略部分代码
    bl  _lookUpImpOrForward
......//省略部分代码

.endmacro

简要流程:
CheckMissJumpMiss调用到__objc_msgSend_uncached,再调用MethodTableLookup,最后调用_lookUpImpOrForward。这样就结束快速查找流程,开始进入慢速查找。

你可能感兴趣的:(方法调用(一)-- objc_msgSend快速查找流程)