OC底层探究—— 类之缓存(下)

544 阅读7分钟

我们在上一篇章分析了类的缓存的基本结构,以及缓存方法的获取,那么缓存方法在什么时候进行插入和读取呢,我们还不得而知,这个章节我们来具体分下。

缓存数据的插入和读取流程梳理

缓存数据在什么时候插入,我们只需要找到调用insert函数的地方,我们可以断点进行调试:

截屏2022-04-23 下午5.46.11.png

我们可以看到调用insert之前,是函数log_and_fill_cache()函数的调用,那再往前一步函数调用就是lookUpImpOrForward()函数的调用,我们在源码里找下这个方法log_and_fill_cache()

截屏2022-04-23 下午5.50.00.png

但是我们还是没理清缓存读写操作之前做了什么,这里我们在源码里找到一段注释,可以帮我们理解下:

截屏2022-04-23 下午6.02.41.png

就是说Cachereaders/writers之前,有一步cache_getImp的操作,读取缓存里的imp,而上一步是objc_msgSend,那么这个函数又有什么意义呢,我们下面进行探究。

objc_msgSend————方法快速查找

首先我们用之前写好的类CTPerson调用下方法run,run2,run3,然后把main.m经过clang命令转成main.cpp文件查看下:

截屏2022-04-23 下午8.32.36.png

可以看到我们方法的调用,最终都转换成objc_msgSend()函数的调用,所以方法的调用本质就是消息的发送。objc_msgSend底层是汇编编写的,我们进objc底层源码看下,汇编文件以.s后缀,所以我们注意下这种类型的文件:

截屏2022-04-23 下午9.29.16.png

汇编相关常用指令

我们就看下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__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR // macOS或者模拟器

#define CACHE_MASK_STORAGE CACHE_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的值复制给x15x16此时就是我们之前的p16,也就是isaclass;我们这里看真机流程,所以看CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16这里:

ldr p10, [x16, #CACHE]:将x16CACHE值相加取内存地址放入p10#define CACHE  (2 * __SIZEOF_POINTER__),所以CACHE2 * 816字节,这里的相加其实就相当于内存平移,即class内存平移16个字节大小,想起我们之前讲的类的内存结构,就知道这里获取的是cacheCONFIG_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是按位存储的,我们看下图:

截屏2022-04-24 下午4.21.18.png

我们可以看到在_bucketAndMaybeMask的低48位存储的是buckets_t指针,高16位mask

tbnz p11, #0, //LLookupPreopt\Function :若p11不为0,则跳转LLookupPreopt,若为0,则继续向下执行。

eor p12, p1, p1, LSR #7p1右移7位p1 ^ p1 >> 7 存进p12。这里对的操作就是cache_hash函数里的实现:

截屏2022-04-25 上午10.55.17.png

后面这里为了方便源码调试,我们看模拟器这部分的代码:

and p10, p11, #0x0000ffffffffffffp11 & 0x0000ffffffffffff 得到buckets,然后存进p10

and p12, p1, p11, LSR #48 // x12 = _cmd & mask:这里是p11右移48位获取到mask,然后 p1 & maskp1是sel,那么就是 sel & mask,结果就是bucket所在的索引位置indexp12即索引位index。看下图:

截屏2022-04-24 下午8.30.03.png

下一步汇编就会走到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里存的是impsel,相当于imp存进了p17sel存进了p9
cmp p9, p1p9是我们查到的sel,而p1是我们要查找的sel,它们两个进行比较,若p9==p1,则继续执行代码2: CacheHit即缓存命中,若两者不相等,则执行3: cbz p9, \MissLabelDynamic,即p9是否存在,若不存在,直接执行MissLabelDynamic,若p9存在,则执行cmp p13, p10p10buckets的首地址,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是之前缓存命中时得到的impx10buckets的首地址,x1selx16isa

接着找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, $3eor按位异或^$0 ^ $3 存进 $0,这里进行异或的原因是我们现在获取到imp是经过编码的,进行是为了还原真正的imp地址,看下图:

截屏2022-04-24 下午9.44.06.png

截屏2022-04-24 下午9.44.35.png

从图中可以得知,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取出isaisa & ISA_MASK获取class,接着class进行内存平移获取cachecache_t里有_bucketAndMaybeMaskbucketsmask按位存在里面,通过bucket掩码mask掩码取出bucketsmask,而当时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,后面我们讲这里。