我们在上一篇章分析了类的缓存的基本结构,以及缓存方法的获取,那么缓存方法在什么时候进行插入和读取呢,我们还不得而知,这个章节我们来具体分下。
缓存数据的插入和读取流程梳理
缓存数据在什么时候插入,我们只需要找到调用insert函数的地方,我们可以断点进行调试:
我们可以看到调用insert之前,是函数log_and_fill_cache()函数的调用,那再往前一步函数调用就是lookUpImpOrForward()函数的调用,我们在源码里找下这个方法log_and_fill_cache():
但是我们还是没理清缓存读写操作之前做了什么,这里我们在源码里找到一段注释,可以帮我们理解下:
就是说Cache在readers/writers之前,有一步cache_getImp的操作,读取缓存里的imp,而上一步是objc_msgSend,那么这个函数又有什么意义呢,我们下面进行探究。
objc_msgSend————方法快速查找
首先我们用之前写好的类CTPerson调用下方法run,run2,run3,然后把main.m经过clang命令转成main.cpp文件查看下:
可以看到我们方法的调用,最终都转换成objc_msgSend()函数的调用,所以方法的调用本质就是消息的发送。objc_msgSend底层是汇编编写的,我们进objc底层源码看下,汇编文件以.s后缀,所以我们注意下这种类型的文件:
我们就看下arm64(真机)环境下的,汇编的话从ENTRY _objc_msgSend开始看:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//cmp即compare p0这里是消息接收者的地址,若recevier是person,那么p0就是person的地址
//真机的寄存器地址 x0-x7
//p0是否为0 ---- 消息接收者是否存在
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//若是一个Tagged_pointer类型,执行这里b.le
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//否则 执行b.eq
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
// 这里p16得到class也是 isa & ISA_MASK得到的,我们可以看到相关的操作
// ExtractISA p16, \src(p13), \auth_address
// .macro ExtractISA and(&) $0, $1, #ISA_MASK
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// recevier -> class cache在class里
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
上面汇编我们可以分析出,objc_msgSend()里面首先会判断消息接收者(receiver)是否存在,接着根据receiver获取isa,进而通过isa & ISA_MASK(掩码)获取到class,接着调用CacheLookup(CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached),下面我们找下CacheLookup:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function //p11的0号位置是否为0,不为0,则进入LLookupPreopt
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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 p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
补充点一些架构的宏定义
#if defined(__arm64__) &&__LP64__
#ifTARGET_OS_OSX||TARGET_OS_SIMULATOR// macOS或者模拟器#define
CACHE_MASK_STORAGECACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16 //真机
#endif
#elif defined(arm64) && !LP64
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
我们来一步步过下上面的汇编代码:
mov x15, x16: 将寄存器x16的值复制给x15,x16此时就是我们之前的p16,也就是isa即class;我们这里看真机流程,所以看CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16这里:
ldr p10, [x16, #CACHE]:将x16和CACHE值相加取内存地址放入p10,#define CACHE (2 * __SIZEOF_POINTER__),所以CACHE是 2 * 8 即16字节,这里的相加其实就相当于内存平移,即class内存平移16个字节大小,想起我们之前讲的类的内存结构,就知道这里获取的是cache。CONFIG_USE_PREOPT_CACHES这个值在这里是1,我们在源码中可以找到它的宏定义:
__has_feature(ptrauth_calls): 是判断编译器是否支持指针身份验证功能
ptrauth_calls 指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构
作者:ShawnRufus
链接:www.jianshu.com/p/68763fa31…
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST<br>
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
所以接着我们这里__has_feature(ptrauth_calls)明显是不满足条件的,我们直接看else里的代码:
and p10, p11, #0x0000fffffffffffe:这里是&操作,即p11&0x0000fffffffffffe获取到buckets存进p10。这里为什么会这样呢,我们在cache_t结构体里的_bucketMaybeMask是按位存储的,我们看下图:
我们可以看到在_bucketAndMaybeMask的低48位存储的是buckets_t指针,高16位是mask。
tbnz p11, #0, //LLookupPreopt\Function :若p11不为0,则跳转LLookupPreopt,若为0,则继续向下执行。
eor p12, p1, p1, LSR #7:p1右移7位,p1 ^ p1 >> 7 存进p12。这里对的操作就是cache_hash函数里的实现:
后面这里为了方便源码调试,我们看模拟器这部分的代码:
and p10, p11, #0x0000ffffffffffff:p11 & 0x0000ffffffffffff 得到buckets,然后存进p10;
and p12, p1, p11, LSR #48 // x12 = _cmd & mask:这里是p11右移48位获取到mask,然后 p1 & mask,p1是sel,那么就是 sel & mask,结果就是bucket所在的索引位置index,p12即索引位index。看下图:
下一步汇编就会走到add p13, p10, p12, LSL #(1+PTRSHIFT):
add p13, p10, p12, LSL #(1+PTRSHIFT):首先这里的PTRSHIFT值源码中可以找到,值为3,相当于p12左移4位,然后加上p10,那么就是buckets+p12 << 4,存进p13 ;这里左移4相当于index*16,这里其实buckets内存平移index位,就如我们之前lldb调试一样buckets[1]这样子。
ldp p17, p9, [x13], #-BUCKET_SIZE: #-BUCKET_SIZE:*bucket--,ldp取一对数据到寄存器,bucket里存的是imp和sel,相当于imp存进了p17,sel存进了p9;
cmp p9, p1:p9是我们查到的sel,而p1是我们要查找的sel,它们两个进行比较,若p9==p1,则继续执行代码2: CacheHit即缓存命中,若两者不相等,则执行3: cbz p9, \MissLabelDynamic,即p9是否存在,若不存在,直接执行MissLabelDynamic,若p9存在,则执行cmp p13, p10,p10是buckets的首地址,p13是内存平移后的某个bucket地址,若bucket >= buckets,则执行b.hs 1b即执行这里ldp p17, p9, [x13], #-BUCKET_SIZE,其实这里是do-while循环,直到CacheHit即缓存命中。
若CacheHit缓存命中,我们继续看下这个函数:
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
我们最开始查找CacheLookup时,这里的mode传的NORMAL,所以这里直接执行TailCallCachedImp x17, x10, x1, x16,从注释里,我们也可以清楚知道,x17是之前缓存命中时得到的imp,x10是buckets的首地址,x1是sel,x16是isa。
接着找TailCallCachedImp:
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
br $0 //跳转进行调用 call imp
eor $0, $0, $3:eor按位异或^,$0 ^ $3 存进 $0,这里进行异或的原因是我们现在获取到imp是经过编码的,进行是为了还原真正的imp地址,看下图:
从图中可以得知,bucket进行set的时候,是origin imp ^ cls, 就是我们现在得到的imp,而我们现在获取origin imp 就需要 cache imp ^ cls进行还原 。
这里是正常的从缓存中找到了imp,若始终没找到呢,我们前面也讲到,若不存在,直接执行MissLabelDynamic,我们在CacheLookup(CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached)看到,它对应的入参是__objc_msgSend_uncached,所以后面就研究它。
总结
总体流程,首先判断receiver是否存在,存在根据recevier取出isa,isa & ISA_MASK获取class,接着class进行内存平移获取cache,cache_t里有_bucketAndMaybeMask,buckets和mask按位存在里面,通过bucket掩码和mask掩码取出buckets和mask,而当时insert数据,所在位置的索引通过cache_hash哈希函数得出的(sel & mask),所以通过sel & mask获取到index,然后根据index取出第一次要查找的bucket,然后buckets+index内存平移循环获取下一个bucket{imp,sel},然后拿到查询到的sel与当前p1(传入的sel)进行比对,如果相等,则缓存命中CacheHit,最后进行 imp ^ cls得到真正的imp地址。而始终没找到的话,调用__objc_msgSend_uncached,后面我们讲这里。