cache_t
在源码中的定义
cache_t
的做用
cache_t
的缓存流程c++
上一篇咱们一块儿探索了 iOS
类的底层结构,咱们先回顾下他的定义:程序员
// 在objc-runtime-new.h这个文件发现了这段定义
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 8
cache cache; // formerly cache pointer and vtable 16
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 8
//下面还有不少方法,在这里暂时咱们不关注
};
复制代码
咱们已经介绍了类的几个重要成员,其中重点探索了class_data_bits_t bits
的内部结构,这里面还有一个cache_t
, 一块儿来看一看这个东西。顾名思义就是缓存的意思,那么用来缓存什么呢?
答案是: 缓存方法 。
它的底层是经过散列表(哈希表)的数据结构来实现存储和读取的,用于缓存曾经调用过的方法,再次调用时能够从缓存里面直接读取,提升方法的查找速度。那么接下来咱们详细介绍下这个家伙。算法
cache_t
在源码中的定义先看下类结构的定义:数组
咱们能够看出ISA,superclass分别都占8个字节,而cache_t
是在class首地址平移16字节的位置,接下来咱们看下cache_t
的定义:缓存
struct cache_t {
struct bucket_t *_buckets; // 8字节,*便是指针,指针占 8 字节
mask_t _mask; // 4字节,uint32_t mask_t,int 类型 4 字节
mask_t _occupied; // 4字节,同上
}
复制代码
其中:数据结构
而_buckets是一个数组,数组里面的每个元素就是一个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__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
复制代码
从源码能够可看出bucket_t里面包含了2个参数_imp和_key.less
cache_t
的做用引言里面咱们提到cache_t是用来缓存方法的,那么为何要缓存方法呢,直接调用不能够吗?讲到这里咱们先回顾下方法的查找流程:
正常时候咱们调用方法是周NORMAL
这种形式,也就是普通查找,假设有个person
类的实例方法eat
被调用[person eat]
,咱们来看下系统的查找流程:函数
obj
-> isa
-> obj
的Class
对象 -> method_array_t methods
-> 对该表进行遍历查找,找到就调用,没找到继续往下走obj
的Class
对象 -> superclass
父类 -> method_array_t methods
-> 对父类的方法列表进行遍历查找,找到就调用,没找到就重复本步骤NSObject
-> isa
-> NSObject的Class
对象 -> method_array_t methods
看下,多么复杂和繁琐,可是苹果的工程师就很聪明,在每一个类里面放一个缓存的盒子,你只要调用我就给你发方法的SEL
和IMP
保存下来,下次调用的时候只要根据SEL就能在缓存中很快的获得方法的实现地址,岂不是极大的提升了效率。ui
cache_t
的缓存流程关于流程源码里面有这样一段注释
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
*
* Cache writers (hold cacheUpdateLock while reading or writing; not PC-checked)
* cache_fill (acquires lock)
* cache_expand (only called from cache_fill)
* cache_create (only called from cache_expand)
* bcopy (only called from instrumented cache_expand)
* flush_caches (acquires lock)
* cache_flush (only called from cache_fill and flush_caches)
* cache_collect_free (only called from cache_expand and cache_flush)
复制代码
能够看出读缓存的时候过程很简单,就是调用objc_msgsend
以后经过cache_getImp
去读取函数的地址,因此咱们着重研究下写的流程,咱们看些的过程不少,可是他的入口是从cache_fill
开始的:
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
这个函数:
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);
// 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();
}
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
复制代码
这么大段代码,能够感受到这个是个核心函数,函数内部作了不少的操做,咱们逐行去研究下
首先是判断cls
也就是类是否被初始化,若是没有直接return
,接下来判断cache_getImp(cls, sel)
是否有值,这里应该是防止在多线程的调用中,别的线程也会调用相同的方法,因此判断下是否在别的线程被写入,若是有就return
// 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;
复制代码
接下来是经过调用函数内部使用内存平移,拿出类内部的缓存,而后根据sel
生成一个key
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
复制代码
首先定义newOccupied
等于旧的占用数+1,取出cache_t
中的capacity
也就是缓存的容量值,
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
复制代码
1:若是缓存是是空的,则进行cache->reallocate()
。
2:若是新的占位容量小于等于当前容量的3/4
,则不做处理
3:而后若是新的占位容量大于当前容量的3/4
,则进行扩容处理cache->expand()
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();
}
复制代码
其中cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE)
是对buckets
从新生成,咱们看下他的实现:
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);
// 下面这个就是把旧的bucket_t给抹掉,释放内存
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
复制代码
函数是根据新的newCapacity
生成一个新的Buckets
而后把老的Buckets
给替换掉,最后释放掉老的Bucket
占用的内存空间。
接下来咱们看下cache->expand()
这个函数的调用:
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
*
能进入到扩容的这里面 _mask 是有值的,而且是而且咱们知道获得的oldCapacity是_maks + 1,
申请的一份新的容量是 oldCapacity * 2,咱们能够验证一下开辟两倍的空间是最划算的。
*
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);
}
复制代码
以上咱们可总结出cache
扩容,就是从新申请一个容量是原来2倍的新容量。
在这里咱们有一个疑问就是在容量不够的时候为何要销毁重建呢,那样以前的缓存不就没有了吗,为何保存以前缓存的方法呢?
苹果的程序员在设计这块的时候可能考虑到保存以前的调用cache
,开辟空间以后还要把老的缓存进行内存平移,这样自己缓存是让人节省时间的设计,这样作反而更耗时,不如销毁直接重建来的快速。
扩容和销毁重建的函数咱们已经了解了,那么回到主线,此时Buckets
存储筒已经准备好,接下来就是存储的过程,首先咱们经过cache->find(key, receiver)
来寻找个合适的筒子,咱们看下他是怎么作寻找的:
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
// 经过cache_hash函数 [begin = k & m]计算出key的值 k 对应的index的值 begin,用来记录查询起始索引
mask_t begin = cache_hash(k, m);
// begin赋值给i,用于切换索引
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
// 用这个i从散列表取值,若是取出来的bucket_t 的 key = k,则查询成功,返回bucket_t
// 若是key = 0, 说明在索引i的位置上尚未缓存过方法,一样须要返回该bucket_t,用于终止缓存查询。
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// 这里其实就是找到咱们cache_t中buckets列表里面须要匹配的bucket。
// hack
// 若是此时尚未找到key对应的bucket_t,或者是空的bucket_t,则循环结束,说明查找失败,调用下面的bad_cache函数
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
复制代码
咱们知道Buckets
实际上是一个数组,数组的底层也是个散列表,根据key计算出index值的这个算法称做散列算法。index = @selector(XXXX) & mask
根据&
运算的特色,能够得知最终index <= mask
,而mask = 散列表长度 - 1
,也就是说0 <= index <= 散列表长度 - 1
,这实际上覆盖了散列表的索引范围。
这个函数调用以后咱们获取到了合适的bucket
筒子,接下来判断if (bucket->key() == 0) cache->incrementOccupied()
若是为真也就是筒子没被占用过,那么Occupied
占用数要加一。
最后,调用set(key, imp)
进行填充
bucket->set(key, imp);
复制代码
咱们总结下cache_t
的整体流程:
1: 当一个对象经过
objc_megsend
接收到消息时;首先根据obj
的isa
指针进入它的类对象cls里面。
2: 在obj的cls里面,首先到缓存cache_t里面查询方法message的函数实现,若是找到,就直接调用该函数。
3: 若是上一步没有找到对应函数,在对该cls的方法列表进行二分/遍历查找
4: 若是找到了对应函数,接下来就是对cache_t
进行填充(1) 进行容错判断,准备一些临时变量。
(2) 在每次进行缓存操做以前,首先须要检查缓存容量,若是缓存内的方法数量超过规定的临界值(设定容量的3/4),须要先对缓存进行2倍扩容,原先缓存过的方法所有丢弃,而后将当前方法存入扩容后的新缓存内
(3) 在Buckets
数组里经过散列算法进行查找合适的bucket
(4) 找到以后判断是否曾经占用过,若是没有占用过,那么就把Occupied
加一
(5) 将方法缓存到bucket
中5:调用该方法。
本片类的结构剖析(cache_t)
分析完毕。