上一篇文章已经写过了类的bits
,现在来看看类的方法缓存cache
它是cache_t
类型定义。在oc中,方法是以SEL-IMP
这种成对的形式存放的。
cache
的内存结构
//macos环境
struct cache_t {
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
struct bucket_t *buckets();
//.…
}
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__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }
inline IMP imp(Class cls) const {
uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
if (!imp) return nil;
return (IMP)(imp ^ (uintptr_t)cls);
#else
}
下面来通过调试来验证缓存cache
的内存结构 断点在NSLog(@"");
:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *myClass = [[MyClass alloc]init];
Class pclass = [MyClass class];
[myClass say];
NSLog(@"");
}
return 0;
}
调试结果
$0=0x00000001000022b8
是MyClass类的isa
地址 根据类的结构体内存结构 cache
结构体的内存地址需要偏移16个字节(cache_t *) $1 = 0x00000001000022c8
通过输出*$1
可以获得cache_t
的结构体内容(cache_t) $2
如上图所示
然后通过cache_t.bucket()
可以获取到这个类存放方法缓存
的buckets
数组的首地址,然后可以一次获取缓存过的方法SEL-IMP
方法缓存的流程
缓存cache
的内存结构验证完了 接下来看看 方法是如何缓存的
cache_t
提供了一个void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
方法 这个方法就是用于方法缓存的插入
最主要的流程如下:
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver){
//...
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;//最大值为16
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(sel, imp, cls);
return;
}
if (b[i].sel() == sel) {
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
}
每次有新方法缓存的时候occupied +1
方法缓存的起始容量是INIT_CACHE_SIZE
初始值是4
每当缓存容量的使用>=3/4
时 缓存就会扩容 在原来容量基础上翻倍
容量最大为16
缓存空间的开辟与初始赋值
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
_buckets.store(newBuckets, memory_order::memory_order_release);
_mask.store(newMask, memory_order::memory_order_release);
_occupied = 0;
}
缓存的创建和扩容 都会用到reallocate()
这个函数 内部会依据当前的capacity
最终调用calloc()
这个函数来分配一个内存空间并初始化
每一次新扩容之后进行 _occupied = 0
,_mask = newMask - 1
初始化赋值。
每次扩容之后 旧的缓存被释放 新的缓存空间被创建并且初始化。
不过根据文档的解释 旧的缓存空间不是立刻被释放 而是在稍后的某个点才会被释放。
方法的缓存方式
当有新的方法需要放进缓存空间的时候,新的缓存是如何加入到缓存空间的呢?
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//…
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(sel, imp, cls);
return;
}
//...
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
新的sel
与mask
进行了一次哈希运算得到一个下标,并依据这个下标处存放的内容来进行存放。
读取的时候也只需要sel
通过哈希运算即可快速取得下标从而拿到函数的imp
地址
网友评论