一、cache_t的结构
iOS开发中, 一个类的结构是objc_class. 本文主要分析objc_class中的cache_t这个结构体.
来看一下cacahe_t的代码结构:
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
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);
};
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
二、cache_t的方法缓存
新建一个测试类:
@interface MyTest : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *gender;
- (void)sayHello;
- (void)sayWorld;
- (void)sayCode;
- (void)sayObject;
@end
在第一次调用sayHello方法前打印cache_t, 发现_msak _occupied为0
调用完第一个sayHello:
调用完一次sayHello方法后发现, _msak _occupied均发生了变化, 并且在_buckets指针里还有相应的方法名. 这就意味着它生成了一个缓存列表.(注: 类方法从元类里找缓存)
调用所有方法:
发现mask变成7, 但是其中的_imp竟然是一个空的. 其中肯定存在问题, 我们需要处理一下. 并不是调用一个方法就缓存一个, 需要特殊处理; 首先_mask发生变化了, 就去找一下mask的方法在哪里调用的:
mask_t mask();
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
跟进调用capacity(),发现这样一个方法里调用了 :
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
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);
}
根据代码得知, 获取了当前容量, 对它进行了一个两倍的扩大. 如果是第一次开辟容量, 则直接容量为4.
来找一下什么时候才会进行扩容.全局搜索 expand():
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;//从缓存里得到imp. 命中缓存直接返回
//没有命中缓存
cache_t *cache = getCache(cls);//获取这个cls的缓存
cache_key_t key = getKey(sel);//获取缓存的方法的key
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;//生成occupied(占用量)
mask_t capacity = cache->capacity();//生成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);//寻址:通过key(哈希值)得到一个地址(方法)
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
来进行一下源码跟踪:
2.1、第一次调用方法
开辟了一个大小为3的容量.
2.2、断点第三次调用方法
容量为3, 占用了两个.
2.3、断点第四次调用方法
扩容为8.进入reallocate(oldCapacity, newCapacity);
发现它是对一个旧的缓存进行了释放, 重新生成一个新的buckets. 这就是为什么之前测试调用所有的方法之后发现buckets里的imp的指针为空的原因了.
总结:
- 当一个类第一次调用一次实例方法的时候, 会对这个方法进行一次方法缓存,调用 void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)方法开辟出一个容量为4的空间用来存储方法.
- 当加载的方法缓存个数超过当前容量的四分之三的时候, 就会调用 void cache_t::expand() 方法对当前的容量进行一次扩容, 新的容量为之前容量的2倍,并且调用 void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) 方法对旧缓存进行一次擦除, 重新开辟一个新的空间用来存放方法缓存.