这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
在之前的文章中,我们讲到了NSObject的父类是objc_class,而它包含以下信息
Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
今天我们来探索一下cache_t
1.知识准备
1.1数组
数组是用于储存多个相同类型数据的集合。主要有以下优缺点:
- 优点:访问某个下标的内容很方便,速度快
- 缺点:数组中进行插入、删除等操作比较繁琐耗时
1.2链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。主要有以下优缺点:
- 优点:插入或者删除某个节点的元素很简单方便
- 缺点:查找某个位置节点的元素时需要挨个访问,比较耗时
1.3哈希表
哈希表是根据关键码值而直接进行访问的数据结构。主要有以下优缺点:
- 优点:1、访问某个元素速度很快。 2、插入删除操作也很方便
- 缺点:需要经过一系列运算比较复杂
2.cache的数据结构
类的结构:在objc_class结构体中,由isa、superclass、cache和bits组成。isa和superclass都是结构体指针,各占8字节。故此,使用内存平移:首地址+16字节,即可探索cache的数据结构体。
2.1探索objc源码
找到cache_t的定义
struct cache_t {
private: explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
...
};
_bucketsAndMaybeMask:泛型,传入uintptr_t类型,占8字节union:联合体,包含一个结构体和一个结构体指针_originalPreoptCachestruct:包含_maybeMask、_flags、_occupied三个成员变量,和_originalPreoptCache互斥 我们找到了cache_t的数据结构,但他的作用还不得而知 通过cache_t的各自方法,可以看出它在围绕bucket_t进行增删改查 找到bucket_t的定义
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
...
};
bucket_t中包含sel和imp- 不同架构,
sel和imp的顺序不一样 通过sel和imp不难看出,在cache_t中缓存的应该是方法
2.2cache_t结构图
3.cache底层原理
3.1 insert函数
在cache_t结构体中,找到insert函数
struct cache_t {
...
void insert(SEL sel, IMP imp, id receiver);
...
};
3.2 创建bucket
insert函数,当缓存列表为空时
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
newOccupied:已有缓存的大小+1capacity:值为4(1 << 2),缓存列表的初始容量reallocate函数,首次创建,freeOld传入falsereallocate函数,创建buckets存储桶,调用setBucketsAndMask函数
bucket_t *newBuckets = allocateBuckets(newCapacity);
setBucketsAndMask(newBuckets, newCapacity - 1);
setBucketsAndMask函数,不同架构下代码不一样,以当前运行的非真机代码为例
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
#ifdef __arm__
// ensure other threads see buckets contents before buckets pointer
mega_barrier();
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
// ensure other threads see new buckets before new mask
mega_barrier();
_maybeMask.store(newMask, memory_order_relaxed);
_occupied = 0;
#elif __x86_64__ || i386
// ensure other threads see buckets contents before buckets pointer
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
// ensure other threads see new buckets before new mask
_maybeMask.store(newMask, memory_order_release);
_occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}
- 传入的
newMask为缓存列表的容量-1,用作掩码 - 将
buckets存储桶,存储到_bucketsAndMaybeMask中。强转uintptr_t类型,只存储结构体指针,即:buckets首地址 - 将
newMask掩码,存储到_maybeMask中 _occupied设置为0,因为buckets存储桶目前还是空的
3.3扩容
如果newOccupied + 1小于等于75%,不需要扩容
#define CACHE_END_MARKER 1
if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
// Historical fill ratio of 75% (since the new objc runtime was introduced).
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
CACHE_END_MARKER:系统插入的结束标记,边界作用 超过75%,进行2倍扩容
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
capacity进行2倍扩容,但不能超过65536- 调用
reallocate函数,扩容时freeOld传入truereallocate函数,当freeOld传入true
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
- 创建
buckets存储桶,代替原有buckets,新的buckets容量为扩容后的大小 - 释放原有的
buckets - 原有
buckets中的方法缓存,全部清除
3.4计算下标
insert函数,调用哈希函数,计算sel的下标
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
- 将
capacity - 1作为哈希函数的掩码,用于计算下标
3.5写入缓存
insert函数,得到buckets存储桶
bucket_t *b = buckets();
buckets函数,进行&运算,返回bucket_t类型的结构体脂针,即:buckets首地址
static constexpr uintptr_t bucketsMask = ~0ul;
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
- 不同架构下,bucketsMask的值不一样
~0ul:0b1111111111111111111111111111111111111111111111111111111111111111&运算:如果两个相应的二进制位都为1,则该位的结果值为1- 所以
addr & ~0Ul,结果还是addr使用下标获取bucket,相当于内存平移。如果bucket中不存在sel,写入缓存
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
incrementOccupied函数,对_occupied进行++set函数,将sel和imp写入bucket如果存在sel,并且和当前sel相同,直接return
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
否则,表示哈希冲突
3.6 防止哈希冲突
cache_next函数,不同框架下算法不一样,以当前运行的非真机代码为例:
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
- 在产生冲突的下标基础上,先进行
+1,再和mask进行&运算 在do...while中,调用cache_next函数,直到解决哈希冲突为止
do {
...
} while (fastpath((i = cache_next(i, m)) != begin));
结论:
capacity:缓存列表的容量occupied:已有缓存的大小maybeMask:使用capacity-1的值作为掩码,在哈希算法、哈希冲突中,用于计算下标- 写入缓存时,如果
写入缓存后的大小+边界超过容量的75%,进行扩容- 扩容:创建新的存储桶,释放原有空间
- 原有存储桶中的方法缓存全部清除
- 先进行2倍扩容,再写入缓存
- 使用哈希函数计算下标,使用下标找到
bucket - 判断
bucket中的sel,不存在则写入 - 如果存在
sel,并且和当前sel相同,直接return - 哈希冲突
- 不同框架,算法不一样
- 在产生冲突的下标基础上,先进行
+1,再和mask进行&运算 - 在
do...while中,直到解决哈希冲突为止
3.7 为什么使用3/4扩容
哈希表具有两个影响其性能的参数:初始容量和负载因子
- 初始容量时哈希表中存储桶的数量,初始容量知识创建哈希表时的容量
- 负载因子是在自动增加其哈希表容量之前,允许哈希表获得的满意度的度量 当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将会被重新哈希。即:内部数据结构将被重建。因此哈希表的存储桶大约为两倍 负载因子定义为3/4,在时间和空间成本之间提供了一个很好的折中方案
- 假如负载因子定为1,那么只有当元素填满时才会扩容。虽然可以最大程度的提高空间利用率,但是会增加哈希冲突,因此查询效率会变得低下。所以当加载因子比较大的时候:节省空间资源,增加查找成本
- 假如负载因子定为0.5,到达空间一般的时候就会去扩容。虽然说负载因子比较小可以最大可能的降低哈希冲突,但空间浪费会比较大。所以当加载因子比较小的时候:节省时间资源,耗费空间资源