上一篇文章,咱们探索了,当实例对象调用实例方法时,方法insert
将sel和imp
插入到cache
的过程。如今咱们再来看看,当调用方法时,是如何从cache
中将其取出来的,即sel-imp的快速查找
。缓存
在调用方法时,是怎么跑到insert
方法的呢?markdown
在源码objc_cache.mm
中搜索insert,除了能够看到上篇文章中的insert方法以外,还找到了cache_fill
方法: 是在
cache_fill
中,调用了insert
的,再搜cache_fill
:less
也就是说在
Cache writers
缓存写入中,会进行cache_fill
操做,并且在缓存写入前,会先进行Cache readers
缓存读取的过程,其中有objc_msgSend 和 cache_getImp
。函数
使用Clang
编译以下代码:oop
@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(消息接收者,方法主体,方法参数..)
。性能
消息接收者的意义,在于经过消息接收者才能找到方法的寻根路径
。 ui
#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。spa
源码中全局搜objc_msgSend
,查找到objc-msg-arm64.s
的汇编代码:3d
objc_msgSend函数是全部OC方法调用的核心引擎,负责查找方法的实现,并执行。因调用频率很是高,其内部实现对性能的影响大,因此使用汇编语言来编写内部实现代码。汇编的特色有速度快、参数不肯定性。rest
解析:
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
复制代码
源码:
.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
,分别保存在p1七、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,再次进行比较
快速查找的过程总结: