iOS底层-类的cache探索
前言
之前的文章分析过类的本质
,我们也从源码的角度看到Class
的是objc_class
类型的结构体
,在objc_class
里面有一个非常重要的变cache
,那cache
它到底是什么,它有什么用,它如何工作的呢?先看一段源码
cache探索
由源码可以发现
cache
是cache_t
类型的。由objc_class
内部的实现可以发现cache
在内存中排在superclass
之后,偏移16字节
。后面可以通过实例验证。cache_t
的结构如下
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
.
.//中间内容较多省略
.
void insert(SEL sel, IMP imp, id receiver);
void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
void destroy();
void eraseNolock(const char *func);
static void init();
static void collectNolock(bool collectALot);
static size_t bytesForCapacity(uint32_t cap);
.
.//中间内容较多省略
.
};
结合实例分析cache的结构,先结合objc源码创建类People
#import <Foundation/Foundation.h>
@interface People : NSObject
- (void)eat;
- (void)run;
- (void)sleep;
@end
@implementation People
- (void)eat {
NSLog(@"%s", __func__);
}
- (void)run {
NSLog(@"%s", __func__);
}
- (void)sleep {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *people = [[People alloc] init];
[people eat];
[people run];
[people sleep];
}
return 0;
}
通过LLDB指令查看People的相关输出
由输出可以看到
$0
的地址其实也是类的isa
地址,我们已经知道cache
在类的内存空间是首地址偏移16字节。所有$0
的基础上加上0x10
(16字节),又因为cache
是cache_t
类型,输出相加后的地址得到cache
。我们在输出cache
,即*$1
就可以得到cache
的内部数据。在众多数据当中我们可以发现_maybeMask ,还有一个_occupied,这两个数据是什么,又干什么用的呢?接着我们调用一下对象的方法
[people eat]
,再看下上面的输出有什么不一样呢?此时的输出我们可以看到_maybeMask和_occupied的值都改变了。cache_t的众多方法中inset方法着重研究下
#if __arm__ || __x86_64__ || __i386__
#define CACHE_END_MARKER 1
#elif __arm64__ && !__LP64__
#define CACHE_END_MARKER 0
#elif __arm64__ && __LP64__
#define CACHE_END_MARKER 0
#define CACHE_ALLOW_FULL_UTILIZATION 1
#else
#error unknown architecture
#endif
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
// ...
// 容量处理
se the cache as-is if until we exceed our expected fill ratio.
// occupied 为cache中方法总数
mask_t newOccupied = occupied() + 1;
// 旧数据占内存的容量
unsigned oldCapacity = capacity(), capacity = oldCapacity;//当前cache能存放方法的容量,首次进来时,值为0
// 判断缓存内容是否为空,首次进来执行此分支内容
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity)
//新数据占内存的容量
capacity = INIT_CACHE_SIZE;//4
reallocate(oldCapacity, capacity, /* freeOld */false);//标记是否需要清理旧的内存空间
}
//插入方法后cache的bucktes内存空间中的保存的方法总数 ,CACHE_END_MARKER x86_64架构,其值为1
// 插入方法后缓存中存放方法的总数+1小于等于缓存总容量的3/4时执行
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
//计算缓存容量,进行扩容
else {
// 如果capacity值为0,赋值为INIT_CACHE_SIZE,这里等于4,否则进行原来的2倍扩容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
// 如果超过最大容量MAX_CACHE_SIZE,则赋值为最大容量
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 重新开辟缓存空间,并清理之前的缓存
reallocate(oldCapacity, capacity, true);
}
// 插入数据
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
// 记录缓存方法的总数加1
incrementOccupied();
// 保存SEL和IMP
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
// 判断下标为i的位置存储的数据和要插入的数据是否相等
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
//cache_next获取下一个下标位置,并赋值给i
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
从inset方法实现可以看到三个关键因素:同参数
、容量
、插入
。函数方法cache_t::insert(SEL sel, IMP imp, id receiver)
。参数依次为sel
、imp
、receiver
类型分别是SEL
(方法编号)、IMP
(方法实现)、id
(消息接受者)。通过insert()的实现,我们可以清晰的弄清cache_t的容量处理过程。需要注意以下几点:
-
occupied
的初始值为0
,newOccupied
的值为1
。 -
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2
,INIT_CACHE_SIZE_LOG2 = 2
-
freeOld
用于标记是否需要清理旧的内存空间状态,初始值为false
- 同
cache_fill_ratio(capacity)
返回值为capacity * 3 / 4
-
reallocate
重新开辟缓存空间,并清理之前的缓存
探究数据插入过程
- 调用
buckets()
返回的为bucket_t *
类型。存放着bucket_t类型的数据连续内存。 - bucket_t类型为结构体,成员变量为SEL、IMP,正好为函数入口所传的参数
-
m = capacity - 1
,用于计算数据插入bucket_t *
的下标值 -
begin = cache_hash(sel, m)
,用于获取插入数据时起始位置
mask_t cache_t::occupied() const
{
return _occupied;
}
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
由occupied()的源码可以看到occupied()的返回值为_occupied。在insert()中调用了capacity()。capacity()实现中又调用了mask()。mask()是从内存中读取_maybeMask的值。
reallocate
探索
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
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) {
collect_free(oldBuckets, oldCapacity);
}
}
reallocate()的作用是重新开辟缓存空间,并清理之前的缓存。在调用setBucketsAndMask(newBuckets, newCapacity - 1)时,保存了_bucketsAndMaybeMask和_maybeMask的值。当freeOld成立时,会调用collect_free()清理掉旧的缓存值。
buckets()
探索
struct bucket_t *cache_t::buckets() const
{
// 读取_bucketsAndMaybeMask的内存地址
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
struct bucket_t {
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
// ...
};
由buckets()
的源码可以看到_bucketsAndMaybeMask
的内存地址&bucketsMask
,返回bucket_t *
类型的数据,即存放bucket_t
类型数据的连续存储空间。bucket_t中保存的就是SEL和IMP。
setBucketsAndMask()探索
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
_maybeMask.store(newMask, memory_order_release);
_occupied = 0;
}
setBucketsAndMask()的源码可见,此处保存了_bucketsAndMaybeMask
和_maybeMask
的值。
总结
-
cache
初始化时,_occupied = 0
即从0
开始,开辟默认为4
的存储空间 -
cache
方法时,首先会判断newOccupied + CACHE_END_MARKER
是否大于缓存内存空间的3/4
,若不满足,直接存储目标方法;若满足条件会进行cache
空间扩容,开辟新的且是原来cache
空间两倍大小的存储空间,清除原来旧的存储空间,在新的空间中缓存目标方法,此时的_occupied = 1
。