1、回顾
在程序运行的时候,oc对象在内存中的存储结构是objc_class类型的,objc_class存放着类的方法列表,属性列表,协议列表,成员变量列表
还存放着方法的缓存列表
2、方法缓存列表
思考:为什么要创建方法缓存列表,目的是什么
2.1 方法的调用
假如调用对象方法的时候,首先从对象的方法列表中查询方法,如果不存在,通过superclass查找父类的方法列表,直到找到方法。
按照这样的逻辑执行的话,每次调用方法都会去查询方法,这样会造成资源的浪费
加入存在一个方法的缓存列表,第一次调用方法的时候,都将这个方法缓存起来,那么下次再调用这个方法的时候,就可以直接从缓存列表中读取,不需要再按照流程去查询方法
再次调用使用过的方法的时候,直接从缓存列表中读取
3、缓存列表的内部结构 cache_t
// cache_t 内部主要有三个属性
struct cache_t {
struct bucket_t *_buckets; // 是一个数组 散列表
mask_t _mask; // 散列表的长度 - 1, 散列表的长度 >= 已经缓存的数量
mask_t _occupied; // 已经缓存的方法数量
}
struct bucket_t {
private:
#if __arm64__ // 手机
SEL _sel; // SEL作为key
uintptr_t _imp; // 函数的地址作为value
#else
SEL _sel; // SEL作为key
uintptr_t _imp; // 函数的地址作为value
#endif
}
public:
// 获取sel
inline SEL sel() const { return _sel; }
// 获取imp
inline IMP imp() const {
if (!_imp) return nil;
return (IMP)
ptrauth_auth_and_resign((const void *)_imp,
ptrauth_key_process_dependent_code,
modifierForSEL(_sel),
ptrauth_key_function_pointer, 0);
}
template <Atomicity>
// 赋值 sel作为key imp作为value
void set(SEL newSel, IMP newImp);
};
4、存储与查询方式
/// 查询方法的方式
//通过sel 方法名
bucket_t * cache_t::find(SEL s, id receiver)
{
assert(s != 0);
// 1.获取散列表
bucket_t *b = buckets();
// 2.获取_mask 散列表的长度 - 1
mask_t m = mask();
// 3.方法名作为key ,cache_hash 内部是将sel & mask 获取位置下标
mask_t begin = cache_hash(s, m);
// 4. i 存放的位置
mask_t i = begin;
do {
///如果 为空 或方法相同,返回
if (b[i].sel() == 0 || b[i].sel() == s) {
return &b[I];
}
// 如果方法不一致,将i -1 向上一个位置查询,直到返回结果
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)s, cls);
}
通过源码分析发现,苹果不是单纯的通过遍历的方式查询方法的位置。 而是通过将 sel 与 mask进行 &预算计算出 方法存放的下标,如果下标内已经存放了方法,会继续往上一个位置存放
如果散列表中分配的内存已经存满,会重新分配
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
// 旧的数量
uint32_t oldCapacity = capacity();
// 新的数量 扩容2倍
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);
}
如果缓存列表满了,会清空缓存列表,扩容内存为原来的两倍,等待下次调用方法的时候重新进行缓存,因为 _mask 存放的数量 已经改变了,进行 &运算的时候,肯定是和之前的值不一样,所以需要清空,重新缓存
5、总结
方法缓存的存储方式 为散列表存储
1.通过方法名 & 当前的_mask 的数量 计算出下标
2.将方法存放到下标位置
3.如果当前位置已经存储了别的方法,那么将下标-1,向上存储
4.如果第一个位置也存储了,就冲最后一个位置继续
5.如果全部都满了,就会重新分配内存,重新进行缓存