经过上一个章节,咱们已经知道的类在底层是以什么样的方式存在的,而且类的属性,成员变量和方法的存储也有了必定的了解,可是类的方法是怎么读取的,每次都要从存储的列表中读出来么,是否是又必定的缓存机制呢?咱们开始研究算法
传送门☞iOS底层学习 -类的前世此生(一)数组
cache_t
结构经过查看类的结构,咱们知道isa
是用来指向类信息的,superclass
是父类相关,class_data_bits_t
是用来存储属性,方法等数据的,那么若是有缓存机制的话,必定是存储在cache_t
中了缓存
struct objc_class : objc_object {
// Class ISA; //8
Class superclass; //8
cache_t cache; //16 // formerly cache pointer and vtable
class_data_bits_t bits;
...省略方法等信息...
};
复制代码
经过上一章节,咱们对cache_t
有个初步的了解,结构如图 安全
cache_t
功能cache_t
的底层是一个哈希表存在,用于缓存调用过的方法,提升查找速度,不用每次从class_data_bits_t
进行遍历查找。用哈希表存储时,存储的位置是不肯定的,空间也有必定的浪费,可是时间复杂度比较低,是典型的空间换时间bash
cache_t
定义struct bucket_t *_buckets
struct bucket_t *_buckets
是一个结构体指针less
cache_key_t
为方法的SEL,也就是方法名,MethodCacheIMP
为对应的函数的内存地址从struct bucket_t * find(cache_key_t key, id receiver);
方法能够得出,cache_t
底层的存储是一个以cache_key_t
为key,bucket_t
为value的一个哈希表函数
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);
};
复制代码
mask_t _mask
mask_t _mask
只是一个32位的int值 ,等于(哈希表长度 - 1)post
typedef uint32_t mask_t;
复制代码
mask_t _occupied
同理_occupied
也是一个值,记录了缓存的方法的数量学习
cache_t
流程经过对objc_cache.mm
源码的注释的阅读,咱们能够获得一个缓存读写的大体过程。相关读取的过程,即在方法转发过程当中,获取到已缓存的IMP函数指针,从而得到方法 ,重点在存取的过程,能够从方法cache_fill
开始ui
cache_fill
经过注释咱们得知,存取的过程是须要加锁来保证线程安全的,_collecting_in_critical
相似轮询线程,保证调用,因此,主要实现的主要方法再cache_fill_nolock(cls, sel, imp, receiver);
中进行
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
}
复制代码
在locker
构造时加锁,析构时解锁,正好保护了方法做用域内的方法调用。这和 EasyReact 中大量使用的__attribute__((cleanup(AnyFUNC), unused))
一模一样,都是为了实现自动解锁的效果。
class locker : nocopy_t {
mutex_tt& lock;
public:
locker(mutex_tt& newLock)
: lock(newLock) { lock.lock(); }
~locker() { lock.unlock(); }
};
复制代码
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 was not 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);
}
复制代码
经过上面的源码,咱们能够得出如下主要步骤
if (!cls->isInitialized()) return;
若是类没有进行初始化操做,则不能进行缓存的操做,这个比较好理解if (cache_getImp(cls, sel)) return;
由于有可能其余线程先进行了存储,因此须要再找查找一遍,若是能够找到缓存,则直接返回,不须要进行缓存的存储cache_t *cache = getCache(cls);
和cache_key_t key = getKey(sel);
分别为获取到类的cache_t
对象和根据方法名获取到cache_key_t
对象mask_t newOccupied = cache->occupied() + 1;
和mask_t capacity = cache->capacity();
分别为cache对象的Occupied和mask对象在原基础上+1if (cache->isConstantEmptyCache())
表示cache是只读的,此时,须要执行cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
方法进行从新申请内存else if (newOccupied <= capacity / 4 * 3)
没有超出哈希表3/4容量时,跳过直接进行下面缓存的操做cache->expand();
进行哈希表扩容bucket_t *bucket = cache->find(key, receiver);
根据key进行方法存储cache->incrementOccupied()
Occupied++bucket->set(key, imp);
写入哈希表经过上面的分析,咱们对cache的存储流程有了大致的了解,其中重点的流程在于缓存如何申请空间cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE)
,如何扩容cache->expand();
,如何写入缓存bucket_t *bucket = cache->find(key, receiver);
首先是isConstantEmptyCache()
方法,表示buckets
是一个只读数组。主要逻辑以下
capacity
计算大小,若是小于EMPTY_BYTES
,则直接返回(bucket_t *)&_objc_empty_cache
,二进制运算后为空bool cache_t::isConstantEmptyCache()
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
复制代码
bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true)
{
cacheUpdateLock.assertLocked();
size_t bytes = cache_t::bytesForCapacity(capacity);
// Use _objc_empty_cache if the buckets is small enough.
if (bytes <= EMPTY_BYTES) {
return (bucket_t *)&_objc_empty_cache;
}
// Use shared empty buckets allocated on the heap.
static bucket_t **emptyBucketsList = nil;
static mask_t emptyBucketsListCount = 0;
mask_t index = log2u(capacity);
if (index >= emptyBucketsListCount) {
if (!allocate) return nil;
mask_t newListCount = index + 1;
bucket_t *newBuckets = (bucket_t *)calloc(bytes, 1);
emptyBucketsList = (bucket_t**)
realloc(emptyBucketsList, newListCount * sizeof(bucket_t *));
// Share newBuckets for every un-allocated size smaller than index.
// The array is therefore always fully populated.
for (mask_t i = emptyBucketsListCount; i < newListCount; i++) {
emptyBucketsList[i] = newBuckets;
}
emptyBucketsListCount = newListCount;
if (PrintCaches) {
_objc_inform("CACHES: new empty buckets at %p (capacity %zu)",
newBuckets, (size_t)capacity);
}
}
return emptyBucketsList[index];
}
复制代码
其次,是cache_t::reallocate
方法,这个方法主要是用来申请缓存空间,主要逻辑以下
canBeFreed()
表示缓存空间不为空,若是为空则不须要后续的清空操做bucket_t *oldBuckets = buckets();
获取旧的缓存空间,bucket_t *newBuckets = allocateBuckets(newCapacity);
是指根据传入的空间,生成新的缓存空间,初始值为INIT_CACHE_SIZE
4字节setBucketsAndMask(newBuckets, newCapacity - 1);
设置cache_t
中的属性cache_collect_free(oldBuckets, oldCapacity);
释放旧的缓存空间,在新的缓存空间进行缓存void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache is not 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);
}
}
复制代码
这个方法就是判断若是此时存储大于了缓存空间的3/4时,对缓存空间进行扩容,算法也比较简单粗暴,就是以前缓存空间的2倍大小,完成后调用reallocate
生成空间
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 not grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
复制代码
这个方法就是根据key找到底层哈希表存储的对应的bucket_t
,主要流程以下
cache_hash
经过cache_hash
函数,即key&mask
计算出key值对应的index值 begin,用来记录查询起始索引do while循环
表示用这个i从散列表取值,若是取出来的bucket_t的 key = k
,则查询成功,返回该bucket_t
,若是key = 0,说明在索引i的位置上尚未缓存过方法,一样须要返回该bucket_t
,用于停止缓存查询。i = i-1
,回到上面do循环里面,至关于查找散列表上一个单元格里面的元素,再次进行key值k的比较,当i=0时,也就i指向散列表最首个元素索引的时候从新将mask赋值给i,使其指向散列表最后一个元素,从新开始反向遍历散列表,其实就至关于绕圈,把散列表头尾连起来,不就是一个圈嘛,从begin值开始,递减索引值,当走过一圈以后,必然会从新回到begin值,若是此时尚未找到key对应的bucket_t
,或者是空的bucket_t
,则循环结束,说明查找失败,调用bad_cache
方法。bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
// begin 赋值给 i,用于切换索引
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
复制代码
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
复制代码
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
复制代码
至此,一个cache的基本流程就完成了
cache_key_t_key
和MethodCacheIMP
的方式缓存在类的_buckets
中,初始是一个4字节的哈希表,mask
值为哈希表长度-1。存储时,使用SEL转换为的cache_key_t_key
&mask
来当作下标存入哈希表