方法的本质1--cache_t方法缓存分析


本页所使用的objc runtime 756.2,来自GITHUB

1.概念

1.1 objc_class 结构

前面探索了类的结构,知道了类的结构本质上是objc_class的结构体,而在 C 源码例, objc_class 结构体的结构如下:

struct objc_class : objc_object {
// Class ISA; // 指向类的isa
Class superclass; // 父类
cache_t cache; // 缓存的方法列表 // formerly cache pointer and vtable
class_data_bits_t bits; // 缓存的属性和变量 // class_rw_t * plus custom rr/alloc flags

上一篇文章,具体分析了class_data_bits_t,那么上面的缓存cache_t 还没有展开学习,下面就继续进行讲解。

1.2 Cache_t 结构:

cache_tobjc_class 的重要组成属性,它主要用来存储方法。

struct cache_t {
struct bucket_t *_buckets; // 缓存的方法列表
mask_t _mask; // 散列表的长度
mask_t _occupied; // 缓存过的方法数量
******
}

bucket 的定义

顾名思义是桶 ,装水的桶,装奥特曼的桶……

在这里是一个hash表,计算公式是hash = sel 地址%mask,其中mask 是存放空间的大小,初始值是4。

通过源码查看,可以知道结构体如下:

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__
MethodCacheIMP _imp; // 方法实现
cache_key_t _key; // 方法编号
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif

可以看到,这里缓存了MethodCacheIMP 方法,其中 MethodCacheIMPIMP的子类:

MethodCacheIMP ——对于方法实现

cache_key_t ——对应方法缓存编号

2. 实现

2.1方法缓存入口

  • 入口
    引起我们注意的是如下这段代码:
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}

代码解释:

cache_fill 方法的缓存写入操作

cache_fill_nolock 线程解锁后的缓存写入

  • 断言保护
    这里的方法cache_fill_nolock 就是我们的方法入口,为追求速度,执行的内容是无锁操作下的缓存填充,即对开辟的内存空间,进行方法写入,实现源码如下:
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();

// Never cache before +initialize is done
if (!cls->isInitialized()) return;

// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;

cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
****

代码解释:

  1. cacheUpdateLock.assertLocked() :这里对内存区域锁定进行了跳出断言
  2. if (!cls->isInitialized()) return; 对为初始化内存空间进行跳出断言
  3. if (cache_getImp(cls, sel)) 对缓存空间已有该方法跳出断言

2.2 检查容量

  • 在检查容量之前,cache_t 做了两个操作:
  1. 将类的引用地址转化成了cache 结构体: cache_t *cache = getCache(cls)
  2. 将方法编号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()
{
cacheUpdateLock.assertLocked();

uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}

reallocate(oldCapacity, newCapacity);
}

代码详解:

oldCapacity 定义了当前的容量

  1. 如果 oldCapacity 为空,则立刻开辟大小为4 的空间。
  2. 否则,给当前空间加倍,即 oldCapacity*2,并在新空间内进行缓存空间开辟 reallocate
2.3.2 新建容器reallocate

新开内存空间的操作步骤:

  1. 确定是否可以释放旧空间
  2. 将开辟的空间和内存方法绑定,并将mask 和占位值occupied 归零。
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();

bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);

// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this

assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

setBucketsAndMask(newBuckets, newCapacity - 1);

if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}

代码讲解:

  • 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)
{
***
// 找到对应的 bucket
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}

void cache_t::incrementOccupied()
{
_occupied++; // 占位增加
}
2.4.2 未扩容过

找到之前的bucket,按照编号和方法,写入缓存:

bucket_t *bucket = cache->find(key, receiver);
bucket->set(key, imp);
2.4.3 缓存写入实现
void bucket_t::set(cache_key_t newKey, IMP newImp)
{
assert(_key == 0 || _key == newKey);

// objc_msgSend uses key and imp with no locks.
// It is safe for objc_msgSend to see new imp but NULL key
// (It will get a cache miss but not dispatch to the wrong place.)
// It is unsafe for objc_msgSend to see old imp and new key.
// Therefore we write new imp, wait a lot, then write new key.

_imp = newImp;

if (_key != newKey) {
mega_barrier();
_key = newKey;
}
}

代码详解:

注释中写明了:

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 的写入,并进行最终调用。

这一章节应该是目前分析最难的,花了大概三到四天弄明白,希望这艰难的一步能为以后的分析打好更好的基础。

3.2 流程图


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
方法的本质2_从objc_msgSend谈起 方法的本质2_从objc_msgSend谈起
方法的本质,就是消息传递… 本页所使用的objc runtime 756.2,来自GITHUB 一、引子:Runtime概念我们都知道,在运行OC代码时,类或者对象在调用方法时会用到runtime,那么,到底什么是运行时呢? 寻找一
2020-02-24 李佳
下一篇 
OC底层研究4--类的结构分析 OC底层研究4--类的结构分析
本页所使用的objc runtime 756.2,来自GITHUB 1.概念1.1 类Class 的类型 Class在源码里的真正类型为objc_class的结构体。 先查看源码——在Xcode按下Shift+Command+O,选择
2020-01-12 李佳
  目录