前言
在 类的底层原理(一) 和 类的底层原理(二) 中,分析了关于类的底层结构,包含 isa、superclass、cache、bits。其中 bits 包含类的属性,方法,代理,成员变量等,以及类方法的获取。
下面继续探索类的结构,关于 cache,其底层原理是什么?存在 cache 的意义又是什么?
准备工作
关于架构:
真机:
arm64模拟器:
i386mac:
__86_64__
__LP64__:Unix 和 Unix类的系统

cache_t 结构
在分析 bits 内存偏移量时,分析了关于 cache_t 占用内存字节数。

根据 cache_t 结构,虽然可以看到整体的数据结构,但是确定不了缓存数据保存位置。是_bucketsAndMaybeMask?还是 _originalPreoptCache?还有 sel 和 imp 在哪呢?目前并不知道,但是既然涉及到缓存,必然有增删改查操作。
在
cache_t中查找相关的方法:

插入方法:


所以:在
cache_t中重点是bucket_t。
bucket_t

bucket是抽象意义的桶子,里面装了若干的sel-imp的映射对。
那么整个类关于cache的结构如下:

LLDB 验证SEL和IMP
获取 bucket_t
cache的内存偏移量是16,即0x10

但是直接通过 _bucketsAndMaybeMask 是拿不到数据的。同样的 _originalPreoptCache 的 Value 也获取不到。

再次分析源码找方法,有个 buckets() 方法

于是再次验证

但是还是没有,发现 sel 拿不到:
这一步的结果其实在第一次获取
cache时已经证实了,其中_maybeMask和_occupied都是0,代表没有方法。稍后解释这两个字段的实际意义。
调用实例方法,形成缓存

从 LLDB 打印结果来看,在调用实例方法之后,cache 里面有值了。

再次打印之后,发现还是没有获取到 sel,进行平移之后,index 为 6 时有数据了。
其中
[index]: 为hash内存平移。

获取sel和imp
继续分析下 bucket_t 的方法并找到了 sel() 和 imp() 方法

LLDB 获取 sel 和 imp

这样就能获取 sel 和 imp 的值了。
疑问:
为什么在
6的位置?为什么
_maybeMask值为7?
cache_t 模拟代码分析
代码模拟的好处:
方便我们进行代码验证,而不是每次都是使用
LLDB,因为LLDB一旦出错可能出现野指针的情况,需要重新验证。遇到源码无法调试的情况,可以进行调试。
小规模取样的方式,能对源码的实现逻辑更清晰。
将 class 以及 cache 代码模拟分析:

zl_objc_class对应源码objc_class结构,因为objc_class继承objc_object,所以有隐藏属性ISA。
zl_class_data_bits_t对应源码class_data_bits_t结构,其中friend修饰类不需要,只有bits属性。
zl_cache_t对应源码cache_t结构,其中_bucketsAndMaybeMask保留,联合体互斥原则,只需要包含_maybeMask,_flags,_occupied的结构体,结构体也可以简化成三个属性。
因为最终存储的数据是 bucket_t ,所以还需要模拟下 bucket_t 的实现,由于之前论证 sel 和 imp 是通过 buckets() 获取的,所以具体看一下 buckets() 方法实现:

通过方法分析:
_bucketsAndMaybeMask通过load获取地址,再通过bucketsMask掩码获取bucket_t *数据。其实就是_bucketsAndMaybeMask指向bucket_t *数据。
zl_cache_t 简化结构如下:

代码验证

打印结果:

_occupied为1,_maybeMask为3
多个方法验证
添加实例方法如下:

添加2个方法:

打印结果:

_occupied为2,_maybeMask为3
添加3个方法:

打印结果:

_occupied为1,_maybeMask为7
添加7个方法:

打印结果:

_occupied为5,_maybeMask为7
结论:
_occupied为所占用个数,_maybeMask总容量大小。
类方法不在类的cache中,应该是在元类的cache中。
_maybeMask的值变化是因为扩容,当发生扩容时,_occupied会重新计数。之前的缓存也都被清空。
cache底层机制
想要了解缓存机制,必然要找关于插入的方法,从源码分析,可以找到 insert() 函数。
insert()

首次
newOccupied为1,同时执行isConstantEmptyCache判断,capacity为4,创建容器时,由于oldCapacity为0,所以不需要释放(freeOld为false)关于扩容条件:
__arm__ || __x86_64__ || __i386__或者__arm64__ && !__LP64__时:当容量大于等于3/4扩容。
__arm64__ && __LP64__时:当容量大于等于7/8扩容。且当容量小于等于8时允许占用100%容量。拓展:
cache_fill_ratio存在的意义其实是关于哈希函数中的负载因子,在3/4和7/8空间利用率最高。扩容数量:如果容量不为
0,则为当前容量 * 2,如果为0,则为4。最大值MAX_CACHE_SIZE = 65536。在扩容时直接释放了旧的缓存。
mask = capacity - 1,这就是为什么第一次是3(4-1),第二次扩容之后是7(4*2-1)的原因。占了一位存储的是end_bucket_t,格式为(sel-imp)0x1-buckets 指针地址)
cache_hash计算插入起点hash地址,之后插入时会通过cache_next避免hash碰撞冲突。循环判断通过set函数插入bucket数据。
reallocate

allocateBuckets通过newCapacity获取新的bucket
setBucketsAndMask存储新的bucket和mask释放旧的缓存
allocateBuckets

calloc开辟内存。创建最后一个元素
endBucket存储为SEL-IMP(0x1-bucket address)
setBucketsAndMask

CACHE_MASK_STORAGE_OUTLINED:是指__arm__ || __x86_64__ || i386环境,只有newBuckets存储在_bucketsAndMaybeMask中,意味着进行了强转,_bucketsAndMaybeMask中只有buckets没有mask。_maybeMask没有进行改变,直接使用capacity-1。
CACHE_MASK_STORAGE_HIGH_16 || CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS:是指OSX || SIMULATOR || 64位真机机型,buckets和mask都存储在_bucketsAndMaybeMask中,其中mask << maskShift,此时maskShift为48。
CACHE_MASK_STORAGE_LOW_4:是指低32位机型,buckets和mask都存储在_bucketsAndMaybeMask中,objc::mask16ShiftBits(mask)方法的作用是:计算在16位以下有多少位是0,_bucketsAndMaybeMask也是存的这个个数值。
_bucketsAndMaybeMask.store()设置bucket和mask的最新值重置
_occupied,这里的_occupied不包括自身的地址占用数。关于 内存排序规则(
memory_order_relaxed/memory_order_release) ,请看详解 C++11的6种内存序总结
cache_hash

CONFIG_USE_PREOPT_CACHES:表示arm64环境真机。
sel地址向右平移7,并和sel地址异或。
cache_next

__arm__ || __x86_64__ || __i386__环境下向后插入(+),__arm64__环境下向前插入(-)
(i+1) & mask:向后插入,进行下一个按位与操作。
i ? i-1 : mask:向前插入,直接使用,没有按位与操作,当i = 0时,返回mask,相当于移动到了倒数第二个(最后一个存储的是自身地址)。
cache属性详解 – _bucketsAndMaybeMask 内存分布
buckets() 方法如下:

mask() 方法如下:

__arm__ || __x86_64__ || __i386__:_bucketsAndMaybeMask存储的只有buckets,mask需要直接从_maybeMask字段读取。
64位 OSX || SIMULATOR:(1<<48) - 1,低48位存储buckets,mask存储在高16位 (maskAndBuckets >> maskShift)。
64 位真机:(1 << 44)-1,低44位存储buckets,mask存储在高16位 (maskAndBuckets >> maskShift)。
32位:~((1<<4) -1):高60位存储buckets,mask存储在低4位 (0xffff >> maskShift)。
疑问: 其中在获取 64 位真机 环境下,低44位 存储 buckets,高16位 存储 mask。其中少了4位,在宏定义 64 位真机 中多了一个 maskZeroBits 的字段,如下:

原因是:这
4位为附加位,且必须为零。为objc_msgSend使用。objc_msgSend会使用这些附加位单个指令标明是来自_maskAndBuckets的值。后面再详细探究。

cache整体流程图
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)