欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
写在前面
在上一篇文章中已经全面地介绍了类的结构,但是还剩下一个cache_t cache
没有进行详细的介绍,本文就将从源码层面分析cache_t
一、初探cache_t
1.cache_t结构
如下是类在底层的结构
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
...
}
其中cache_t
的结构如下
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
...
};
之前文章也说过,从cache_t
的结构中可以得出它是由两个uint32_t
类型的_mask
和_occupied
以及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
的属性和方法中可以看出它应该与imp
有联系——事实上bucket_t
作为一个桶,里面是用来装imp
方法实现以及它的key
cache_t
中的_buckets
、_mask
、_occupied
从字面意思上理解为桶
、面具
、占据
,但是我们不知道这三个的作用是否与他们的名字有关系,下面我们先从LLDB打印一些信息来看看
2.LLDB调试
在objc源码
准备好代码
#import <objc/runtime.h>
@interface FXPerson : NSObject
- (void)doFirst;
- (void)doSecond;
- (void)doThird;
@end
@implementation FXPerson
- (void)doFirst {}
- (void)doSecond {}
- (void)doThird {}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *p = [[FXPerson alloc] init];
Class cls = object_getClass(p);
[p doFirst];
[p doSecond];
[p doThird];
}
return 0;
}
_buckets
是一个装imp
方法实现的桶,那我们在方法调用的时候打个断点(上篇文章讲过,类中isa指针
占8字节,superclass指针
占8字节,只要拿到类的首地址+16字节
就能得到cache_t
的地址)
此时_mask
为3,_occupied
为1,我们继续打印_buckets
打印了多个$3
只发现缓存了一个[NSObject init]
,心中不免有了一个想法
断点来到[p doSecond];
一行(笔者这里重新跑项目了)
断点来到[p doThird];
一行,得到如下数据:
断点处 | _occupied | _buckets包含方法 |
---|---|---|
[p doFirst] | 1 | -[NSObject init] |
[p doSecond] | 2 | -[NSObject init]、-[FXPerson doFirst] |
[p doThird] | 3 | -[NSObject init]、-[FXPerson doFirst]、-[FXPerson doSecond] |
上述数据可以得出_buckets
是个装方法实现的桶子,_occupied
数值是桶子中有多少个方法实现
等等,这里肯定有人还有疑问,FXPerson调用了alloc方法,怎么都没缓存——上一篇文章已经讲过了,alloc方法属于类方法,存在FXPerson元类中
本以为一切都顺顺利利的时候,意外发生了——断点走到下一行
_mask
和_occupied
都发生了不可思议的变化,那么底层到底做了什么呢?为什么先前打印bucket[0]
的时候全为空呢?
二、深入cache_t
0.找到切入点
已知_mask
的值是增加了,所以我们找到cache_t
中的mask_t mask()
方法,结果只返回了_mask
本身
mask_t cache_t::mask()
{
return _mask;
}
继续搜索mask()
方法,发现在capacity
方法中有mask的相应操作,但是操作目的不是很明确
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
继续搜索capacity()
方法,在expand方法中看到了capacity
方法的有意义调用
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);
}
expand
方法应该是个扩容方法,继续往上摸,摸到了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);
}
加个断点在函数调用栈中验证了我们找的方向是正确的
1.cache_fill_nolock
cache_fill_nolock方法比较复杂,笔者这里将一步步分析
①if (!cls->isInitialized()) return;
类是否初始化对象,没有就返回
②if (cache_getImp(cls, sel)) return;
传入cls
和sel
,如果在缓存中查找到imp
就返回,不能就下一步
③cache_t *cache = getCache(cls);
调用getCache
来获取cls
的缓存对象
④cache_key_t key = getKey(sel);
通过getKey
来获取到缓存的key——其实是将SEL
类型强转成cache_key_t
类型
⑤mask_t newOccupied = cache->occupied() + 1;
在cache
已经占用的基础上进行加 1,得到的是新的缓存占用大小 newOccupied
⑥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();
}
- 如果缓存为空,重新申请一下内存并覆盖之前的缓存
- 如果新的缓存占用大小
<=
缓存容量的四分之三,则可以进行缓存流程 - 如果缓存不为空,且缓存占用大小已经超过了容量的四分之三,则需要进行扩容
⑦bucket_t *bucket = cache->find(key, receiver);
通过key
在缓存中查找到对应的bucket_t
⑧if (bucket->key() == 0) cache->incrementOccupied();
如果⑦找到的bucket
中key
为0,那么_occupied++
⑨bucket->set(key, imp);
把key
、imp
成对放入bucket
总结:
cache_fill_nolock
先找到类的缓存cache
,如果缓存cache
为空就创建并覆盖;如果目标占用(缓存之后的占用大小newOccupied
)大于缓存容量的四分之三,先扩容再装入对应key
值的桶内bucket
;否则直接装入对应key
值的桶内bucket
分析完cache_fill_nolock主流程,再根据一些方法进行扩展
2.cache_t::reallocate
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);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
bucket_t *allocateBuckets(mask_t newCapacity)
{
// Allocate one extra bucket to mark the end of the list.
// This can't overflow mask_t because newCapacity is a power of 2.
// fixme instead put the end mark inline when +1 is malloc-inefficient
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);
bucket_t *end = cache_t::endMarker(newBuckets, newCapacity);
#if __arm__
// End marker's key is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->setKey((cache_key_t)(uintptr_t)1);
end->setImp((IMP)(newBuckets - 1));
#else
// End marker's key is 1 and imp points to the first bucket.
end->setKey((cache_key_t)(uintptr_t)1);
end->setImp((IMP)newBuckets);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
// objc_msgSend uses mask and buckets with no locks.
// It is safe for objc_msgSend to see new buckets but old mask.
// (It will get a cache miss but not overrun the buckets' bounds).
// It is unsafe for objc_msgSend to see old buckets and new mask.
// Therefore we write new buckets, wait a lot, then write new mask.
// objc_msgSend reads mask first, then buckets.
// ensure other threads see buckets contents before buckets pointer
mega_barrier();
_buckets = newBuckets;
// ensure other threads see new buckets before new mask
mega_barrier();
_mask = newMask;
_occupied = 0;
}
- 先判断能否被释放(缓存是否为空的取反值)并保存
oldBuckets
获取到当前bucket
- 传入新的缓存容量
allocateBuckets
初始化bucket_t
,保存在newBuckets
setBucketsAndMask
做的操作: 用新创建的bucket
保存,mask=newcapcity-1
,occupied
置零(因为还没有方法缓存)- 如果缓存不为空(需要释放)则释放原先的
bucket
、capacity
为什么使用cache_collect_free消除记忆,而不是重新读写、内存拷贝的方式?一是重新读写不安全;二是抹掉速度快
3.cache_t::expand
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);
}
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
oldCapacity
的值为mask+1
- 在
oldCapacity
存在的情况下,newCapacity
取oldCapacity
的两倍;否则取INIT_CACHE_SIZE
- 这里的
INIT_CACHE_SIZE
为二进制的100
=>十进制的4
- 创建并覆盖原来的缓存
reallocate
4.cache_t::find
cache_t::find
是 找对应的存储桶
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);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
- 通过
buckets()
方法获取当前cache_t
下所有的缓存桶bucket
- 通过
mask()
方法获取当前cache_t
的缓存容量减一的值mask_t
key & mask
计算出起始索引begin
赋值给i
,用于切换索引- 在
do-while
循环里遍历整个bucket_t
,如果key = 0
,说明在索引i的位置上还没有缓存过方法,同样需要返回该bucket_t
,用于中止缓存查询;如果取出来的bucket_t
的key = k
,则查询成功,返回该bucket_t
- 通过
cache_next
返回i-1
来更新索引,以此来查询散列表中的每一个元素(相当于绕圈) - 如果找不到证明缓存有问题,返回
bad_cache
5.LRU算法
LRU算法
的全称是Least Recently Used
,也就是最近最少使用策略——这个策略的核心思想就是先淘汰最近最少使用的内容,在方法缓存中也用到了这种算法
- 在扩容前,实例方法随便选择位置坐下
- 在扩容后,新的实例方法找到最近最少使用的位置坐下并清掉之前的bucket
三、cache_t疑问点
1.mask的作用
mask
是作为cache_t
的属性存在的,它代表的是缓存容量的大小减一的值mask
对于bucket
来说,主要是用来在缓存查找时的哈希算法
2.capacity的变化
capacity
的变化主要发生在扩容cache->expand()
的时候,当缓存已经占满了四分之三的时候,会进行两倍原来缓存空间大小的扩容,这一步是为了避免哈希冲突
3.为什么是在 3/4 时进行扩容
在哈希这种数据结构里面,有一个概念用来表示空位的多少叫做装载因子
——装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降
负载因子是3/4
的时候,空间利用率比较高,而且避免了相当多的Hash冲突,提升了空间效率
具体可以阅读HashMap的负载因子为什么默认是0.75?
4.方法缓存是否有序
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
方法缓存是无序的,因为是用哈希算法来计算缓存下标——下标值取决于key
和mask
的值
5.bucket与mask、capacity、sel、imp的关系
- 类
cls
拥有属性cache_t
,cache_t
中的buckets
有多个bucket
——存储着方法实现imp
和方法编号sel
强转成的key值cache_key_t
mask
对于bucket
来说,主要是用来在缓存查找时的哈希算法capacity
则可以获取到cache_t
中bucket
的数量
缓存的主要目的就是通过一系列策略让编译器更快的执行消息发送的逻辑
写在后面
关于cache_t
的内容虽然不多但还是蛮绕的,多读读源码会有更深的理解。下篇文章讲objc_msgsend
,作为cache_fill_nolock
前置方法,一定程序上会对cache_t
的理解有所帮助