iOS底层-cache_t原理分析
前言
在 类的底层原理(一) 和 类的底层原理(二) 中,分析了关于类的底层结构,包含 isa
、superclass
、cache
、bits
。其中 bits
包含类的属性,方法,代理,成员变量等,以及类方法的获取。
下面继续探索类的结构,关于 cache
,其底层原理是什么?存在 cache
的意义又是什么?
准备工作
关于架构:
真机:
arm64
模拟器:
i386
mac:
__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
时有数据了。
获取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
的值。后面再详细探究。