上一篇文章,我们探索了,当实例对象调用实例方法时,方法insert将sel和imp插入到cache的过程。现在我们再来看看,当调用方法时,是如何从cache中将其取出来的,即sel-imp的快速查找。
objc_msgSend铺垫
1. 源码查看
在调用方法时,是怎么跑到insert方法的呢?
在源码objc_cache.mm中搜索insert,除了可以看到上篇文章中的insert方法之外,还找到了cache_fill方法:
是在
cache_fill中,调用了insert的,再搜cache_fill:
也就是说在
Cache writers缓存写入中,会进行cache_fill操作,而且在缓存写入前,会先进行Cache readers缓存读取的过程,其中有objc_msgSend 和 cache_getImp。
2. Clang
使用Clang编译如下代码:
@interface Person : NSObject
- (void)sayHello;
- (int)addNumber:(int)number;
@end
@implementation Person
- (void)sayHello{
NSLog(@"Hello world");
}
- (int)addNumber:(int)number{
return number+1;
}
@end
Person *p = [Person alloc];
[p sayHello];
int result = [p addNumber:2];
在cpp文件中,我们可以看到编译后的结果:
无论是调用
类方法alloc,还是实例方法sayHello,都会编译成objc_msgSend(消息接收者,方法主体,方法参数..)。
消息接收者的意义,在于通过消息接收者才能找到方法的寻根路径。
3. 拓展
#import <objc/message.h>
@interface Tercher : Person
@end
@implementation Tercher
@end
Person *p = [Person alloc];
[p sayHello];
objc_msgSend(p, sel_registerName("sayHello"));
Tercher *t = [Tercher alloc];
[t sayHello];
struct objc_super xsuper;
xsuper.receiver = t;
xsuper.super_class = [Person class];
objc_msgSendSuper(&xsuper, sel_registerName("sayHello"));
需要设置
Build Settings->Enable Strict Checking of objc_msgSend Calls为NO。
objc_msgSend快速查找
objc_msgSend
源码中全局搜objc_msgSend,查找到objc-msg-arm64.s的汇编代码:
objc_msgSend函数是所有OC方法调用的核心引擎,负责查找方法的实现,并执行。因调用频率非常高,其内部实现对性能的影响大,所以使用汇编语言来编写内部实现代码。汇编的特点有速度快、参数不确定性。
解析:
1.
cmp p0, #0 // nil check and tagged pointer check
第一段代码cmp(compared对比),我们从注释就可以看到nil check判断是否为nil。其中p0是objc_msgSend的第一个参数消息接收者receiver,那么这句代码的含义就是:判断接收者是否存在。
2.
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
SUPPORT_TAGGED_POINTERS判断是否支持小对象类型,支持会b.le跳转到LNilOrTagged,否则b.eq LReturnZero返回空。
当支持小对象类型时,仍会由cmp p0, #0的结果来决定是否继续,消息接收者为空则同样调用LReturnZero。
le = less equal 小于等于; eq = equal 等于
3.
ldr p13, [x0] // p13 = isa
根据对象拿到isa存入p13寄存器中。
4.
GetClassFromIsa_p16 p13 // p16 = class
在64位真机中,将
$0(传入的p13->isa)和ISA_MASK掩码进行与运算,可以得到class类信息,查找到类信息后,就可以偏移到cache进行方法的查找,即CacheLookup NORMAL 快速查找。
5.
LGetIsaDone: // 获取isa完毕
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
CacheLookup NORMAL
源码:
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
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
1.
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
其中#CACHE == 2*8 = 16为:
由类结构可知,将isa位移16个字节,可以得到cache,即最终结果
p11=cache。但是注释为什么是p11 = mask|buckets? 原因在于:在64位系统下为了节省内存读取方便,mask和buckets存在了一起,cache的结构:
2.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
将p11(mask|buckets)和0x0000ffffffffffff进行与运算,会将其高16位进行抹零,得到的结果就是buckets,存入p10。
and p12, p1, p11, LSR #48分为两段,首先计算p11, LSR #48,将p11进行逻辑右移48位,即可得到cache中的mask。然后将p1与运算mask的结果存在p12中。其中p1为sel(_cmd)。看CacheLookup源码中最开头的注释里。
最终与运算的结果
p12,就是方法存在buckets的下标。
因为在上篇文章,insert方法中要插入的位置,就是利用
sel & mask计算得到的下标.
3.
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
这里也分为两段看待:
-
p12, LSL #(1+PTRSHIFT)。全局搜索PTRSHIFT:64位真机下,
PTRSHIFT = 3,那么第一段代码的含义就是将方法的下标进行逻辑左移4位。左移4位也等同于2^4。0000 0001 << 4 = 0001 0000 = 16 = 2^4所以上段汇编代码的含义就是,
方法的下标 * 2^4。得到的结果存入p10。 -
add p12, p10
p12保存的buckets的首地址,这段汇编就是从首地址,偏移方法下标 * 2^4个字节,得到要查找的方法bucket_t。
为什么下标乘上16个字节?原因在于bucket_t中保存的是sel和imp,都为8个字节,一个bucket_t就是16字节,所以下标乘每个bucket_t的大小,就可以找到,下标所指的bucket_t。bucket_t等同于汇编中的bucket,bucket_t是C语言中的结构体,bucket是汇编的。
4.
ldp p17, p9, [x12] // {imp, sel} = *bucket
以上通过得到的方法所在的bucket,也就找到了其中的imp和sel,分别保存在p17、p9。
5.
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
这段汇编代码的含义,在注释中也可以很清楚的了解,通过对比查找到的bucket中的sel是否等于CacheLookup传入的参数p1(_cmd),不相等b.ne 2f跳转到第2步,相等则CacheHit $0 缓存命中,进行imp返回
6.
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
当查找到的sel不等于参数p1(_cmd)时,首先会判断查找到的bucket是否等于buckets,即是否是buckets的开头,相等会跳到3,否则ldp p17, p9, [x12, #-BUCKET_SIZE]!。即不相等时,会向前找bucket,再次跳转到1进行loop循环。
7.
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
当查找到的bucket等于buckets,即等于开头第一个时:add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
其中p11在最开始就知道p11 = mask|buckets,p11逻辑右移44位,也可以认为是p11中的mask左移了4位,即注释中 (mask << 1+PTRSHIFT) == mask * 2^4。
在上篇文章中,mask的值等于capacity-1,即buckets中所有的结构体个数减一。
所以这句汇编的含义就是:当查找的bucket等于buckets中第一个时,会偏移到最后一个bucket,再次进行比较
快速查找的过程总结: