这是我参与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缓存
数组是用于储存多个相同类型数据的集合。主要有如下优缺点:markdown
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的。主要有如下优缺点:数据结构
哈希表是根据关键码值而直接进行访问的数据结构。主要有如下优缺点:架构
类的结构:在objc_class
结构体中,由isa
、superclass
、cache
和bits
组成。isa
和superclass
都是结构体指针,各占8字节
。故此,使用内存平移:首地址+16字节
,便可探索cache的数据结构体。框架
找到cache_t的定义less
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
:联合体,包含一个结构体和一个结构体指针_originalPreoptCache
struct
:包含_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
中缓存的应该是方法post
insert
函数在cache_t
结构体中,找到insert
函数
struct cache_t {
...
void insert(SEL sel, IMP imp, id receiver);
...
};
复制代码
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
传入false
reallocate
函数,建立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
存储桶目前仍是空的若是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
传入true
reallocate
函数,当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
中的方法缓存,所有清除insert
函数,调用哈希函数,计算sel
的下标
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
复制代码
capacity - 1
做为哈希函数的掩码,用于计算下标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);
}
复制代码
~0ul
:0b1111111111111111111111111111111111111111111111111111111111111111
&
运算:若是两个相应的二进制位都为1,则该位的结果值为1addr & ~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;
}
复制代码
不然,表示哈希冲突
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%
,进行扩容
bucket
bucket
中的sel
,不存在则写入sel
,而且和当前sel
相同,直接return
+1
,再和mask
进行&
运算do...while
中,直到解决哈希冲突为止哈希表具备两个影响其性能的参数:初始容量和负载因子
当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将会被从新哈希。即:内部数据结构将被重建。所以哈希表的存储桶大约为两倍 负载因子定义为3/4,在时间和空间成本之间提供了一个很好的折中方案