本页所使用的objc runtime 756.2,来自GITHUB
1.概念
1.1 objc_class 结构
前面探索了类的结构,知道了类的结构本质上是objc_class的结构体,而在 C 源码例, objc_class 结构体的结构如下:
struct objc_class : objc_object { |
上一篇文章,具体分析了class_data_bits_t,那么上面的缓存cache_t 还没有展开学习,下面就继续进行讲解。
1.2 Cache_t 结构:
cache_t 是objc_class 的重要组成属性,它主要用来存储方法。
struct cache_t { |
bucket 的定义
顾名思义是桶 ,装水的桶,装奥特曼的桶……
在这里是一个hash表,计算公式是hash = sel 地址%mask,其中mask 是存放空间的大小,初始值是4。
通过源码查看,可以知道结构体如下:
struct bucket_t { |
可以看到,这里缓存了MethodCacheIMP 方法,其中 MethodCacheIMP 是IMP的子类:
MethodCacheIMP ——对于方法实现
cache_key_t ——对应方法缓存编号
2. 实现
2.1方法缓存入口
- 入口
引起我们注意的是如下这段代码:
void cache_fill(Class cls, SEL sel, IMP imp, id receiver) |
代码解释:
cache_fill 方法的缓存写入操作
cache_fill_nolock 线程解锁后的缓存写入
- 断言保护
这里的方法cache_fill_nolock 就是我们的方法入口,为追求速度,执行的内容是无锁操作下的缓存填充,即对开辟的内存空间,进行方法写入,实现源码如下:
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) |
代码解释:
- cacheUpdateLock.assertLocked() :这里对内存区域锁定进行了跳出断言
- if (!cls->isInitialized()) return; 对为初始化内存空间进行跳出断言
- if (cache_getImp(cls, sel)) 对缓存空间已有该方法跳出断言
2.2 检查容量
- 在检查容量之前,cache_t 做了两个操作:
- 将类的引用地址转化成了cache 结构体:
cache_t *cache = getCache(cls)
- 将方法编号
sel
转换成了整型,方便寻址cache_key_t key = getKey(sel)
容量为空检测
将方法缓存时,先确定是否为空对缓存。这里使用了
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}来判断空间是否为空,
isConstantEmptyCache
这个函数更详细的操作如下:return occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);即
occupied
占位为空,而且容器桶 也无法从其他堆中空间共享空间,就必须重新开辟新的空间,开辟空间操作见 2.3。扩容的条件:
扩容条件的操作为将当前容量 occupied + 1, 然后检查是否达到 3/4,超过则需要扩容;否则不需要扩容,直接进行缓存的写入,下面的代码足够明了的解释;
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}代码详解:
mask_t newOccupied = cache->occupied() + 1
mask_t capacity = cache->capacity();
if (newOccupied <= capacity / 4 * 3)
这里的 newOccupied 是当前的占用容量+1, 与目前的总容量 capacity 的 3/4 来做比较,这里使用占位+1 后来做比较,目的是提前准备,防止内存溢出。
2.3 内存扩容
2.3.1 空间计算
扩容方法:
判断当前容量是否为空,若为空,就给初始化的内存为为4;
如果之前就有空间,则加倍。
void cache_t::expand() |
代码详解:
oldCapacity 定义了当前的容量
- 如果 oldCapacity 为空,则立刻开辟大小为4 的空间。
- 否则,给当前空间加倍,即 oldCapacity*2,并在新空间内进行缓存空间开辟 reallocate。
2.3.2 新建容器reallocate
新开内存空间的操作步骤:
- 确定是否可以释放旧空间
- 将开辟的空间和内存方法绑定,并将mask 和占位值occupied 归零。
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) |
代码讲解:
bool freeOld = canBeFreed() 决定了是否可以释放旧内存。
bool cache_t::canBeFreed()
{
return !isConstantEmptyCache();
}这里的
canBeFreed
依赖于isConstantEmptyCache
的取反,即需要之前方法缓存有占位,并且旧bucket 本身容量不为空。即以下源码里返回为空:
bool cache_t::isConstantEmptyCache()
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
需要 occupied == 1, 以及 buckets() != emptyBucketsForCapacity(capacity(), false)setBucketsAndMask 用来初始化 新的bucket 和 occupied
newCapacity - 1 更新索引,用来查询散列表里的元素。
cache_collect_free 用来释放旧容量(oldCapacity)下的旧方法数据(oldBuckets)
2.4 缓存写入
2.4.1 查找缓存
通过 cache->find
查找对应的缓存,如果没有找到,就添加新缓存,在这之前将占位occupied
添加1 ,实现源码如下:
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) |
|
2.4.2 未扩容过
找到之前的bucket,按照编号和方法,写入缓存:
bucket_t *bucket = cache->find(key, receiver); |
2.4.3 缓存写入实现
void bucket_t::set(cache_key_t newKey, IMP newImp) |
代码详解:
注释中写明了:
objc_msgSend 在无锁环境下使用key 和 imp 实现。
objc_msgSend 查看到新的imp 实现是安全的,除了空的key地址外。(可能会造成小的缓存丢失,但是并不会分发到错误的空间)
objc_msgSend 旧的imp 和新的key 是不安全的,因此我们先写新imp,等一会儿,再写新的 key
代码中的
mega_barrier
就是 使用了阻塞,让方法先看到imp ,保证线程的安全。
3. 总结
3.1 梳理
cache_t 起源于 OC中的方法传递,也就是objc_msgSend 的实现。在类的方法传递时,为了追求,先去cache_t 中查找是否有缓存,如果有,可以直接调用,如果没有缓存,则需要对类进行一系列的内存空间确认,进行imp - key 的写入,并进行最终调用。
这一章节应该是目前分析最难的,花了大概三到四天弄明白,希望这艰难的一步能为以后的分析打好更好的基础。