cache_t底层分析
类的本质我们已经分析完了,里面有isa
、superclass
、cache
、bits
。
今天对cache
进行研究。在objc
源码中可以看到cache
的类型是cache_t
,用lldb
调试打印cache
的所有数据:
在这里看的数据会感觉比较混乱,那我们从源码中分析,最后再回来验证。
在struct cache_t
结构中,我们根据提供的方法来分析里面的数据结构,找到一行关键代码void insert(SEL sel, IMP imp, id receiver);
,缓存中插入数据,根据参数我们一下就能知道cache
里面缓存的是方法了。点击insert
方法看里面都有哪些操作:
发现里面有bucket_t
这个数据结构,方法就插入到这里面,再看下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__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
}
bucket_t
里面就有两个成员,一个_imp
,一个_sel
,这下我们心里大概能有个概念,cache
是缓存方法的,里面有个bucket_t
数组,数组每一项里面存着imp
和sel
。
虽然得出了一些结论,但是还是要通过lldb进行验证。因为cache_t
里面没有bucket_t
这个成员,所以就要找相关方法看有没有能获取的,正好找到struct bucket_t *buckets() const;
这个方法可以获取,那我们进行验证。
输出发现是有bucket_t
,但是里面的值都是空的,怎么回事呢?以为缓存是缓存的方法,你什么方法都不执行哪来的缓存,我们重新执行下代码。
SJPerson *p = [SJPerson alloc];
[p doSomething];
image
image
我们看到上面Value
是3,打印出来为什么还是没有数据呢?那我们试图打印buckets
后面几个数据试试:
发现第2个元素有值了,为了验证是不是我们的doSomething
方法,在bucket_t
结构里面找对应的输出方法,继续调试
发现确实是我们的方法,没有问题。
接下来我们对cache_t
进行原理分析。既然是缓存,肯定有读写相应的操作,我们从写作为入口进行分析,为什么不从读作为入口呢,因为你不先写,读也读不出来数据。在cache_t
中的方法void insert(SEL sel, IMP imp, id receiver);
,我们点进去看下插入都做了什么操作。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
/// occupied(): return _occupied; 成员变量,默认0
mask_t newOccupied = occupied() + 1; // 这里+1是为了把这次执行的方法先加上
unsigned oldCapacity = capacity(), capacity = oldCapacity;
/// 第一个if:cache是空
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
/// capacity=0时 初始设置 capacity = INIT_CACHE_SIZE = 4,真机为2
if (!capacity) capacity = INIT_CACHE_SIZE;
/// 申请缓存的内存空间,具体代码在下一段
reallocate(oldCapacity, capacity, /* freeOld */false);
}
/// 如果 newOccupied + 1 <= 3/4 * capacity,还可以继续插入,对桶子不做操作,真机的负载因子是7/8
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.
}
/// arm64&&LP64 即真机环境下 CACHE_ALLOW_FULL_UTILIZATION为1
#if CACHE_ALLOW_FULL_UTILIZATION
/// 如果 capacity<=1<<3,即8 && newOccupied <= capacity,还可以继续插入,对桶子不做操作
/// 真机容量小于等于8,可以全填满,大于8时真机用7/8
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时,capacity = 4,不为0时 capacity *2直接扩容到原来的2倍
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
/// 重新申请内存
reallocate(oldCapacity, capacity, true);
}
/// 插入的具体操作
/// 获取桶子
bucket_t *b = buckets();
mask_t m = capacity - 1;
/// 利用hash算法计算下标开始值,供下面循环使用,算法真机环境稍微有所区别
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
/// 一个循环,退出条件:1、成功插入 2、已经被插入 3、找一圈下标也没合适位置插入。3个条件满足一个即退出循环,最后一种情况会走bad_cache
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
/// 如果桶子的i位置没有存数据,则直接插入
if (fastpath(b[i].sel() == 0)) {
/// _occupied++; 也就是说_occupied是记录方法插入的个数
incrementOccupied();
/// 插入方法,真机和非真机有小区别
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
/// 如果当前位置插入的有数据并且与执行的方法相同,直接return
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
/// 当前位置有数据,获取下一个坐标,并且坐标不等于开始坐标,继续循环
/// cache_next实现在下面
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
/**
* 模拟器,mac环境CACHE_END_MARKER为1
* 负载因子3/4
* CACHE_END_MARKER为1,buckets填充结束标志
*/
#if __arm__ || __x86_64__ || __i386__
#define CACHE_END_MARKER 1
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
#elif __arm64__ && !__LP64__
#define CACHE_END_MARKER 0
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
/**
* 真机环境 CACHE_END_MARKER为0
* 负载因子7/8
* CACHE_END_MARKER为0,buckets不用填充结束标志
* CACHE_ALLOW_FULL_UTILIZATION为1,小容量缓存允许充满
*/
#elif __arm64__ && __LP64__
#define CACHE_END_MARKER 0
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 7 / 8;
}
#define CACHE_ALLOW_FULL_UTILIZATION 1
#else
#error unknown architecture
#endif
enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
INIT_CACHE_SIZE_LOG2 = 2,
#else
INIT_CACHE_SIZE_LOG2 = 1,
#endif
/// 真机环境INIT_CACHE_SIZE=2,模拟器mac INIT_CACHE_SIZE=4
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
/// 旧桶子
bucket_t *oldBuckets = buckets();
/// 新桶子根据newCapacity来申请,申请的具体代码在下一段
bucket_t *newBuckets = allocateBuckets(newCapacity);
/// 设置_bucketsAndMaybeMask成员变量
setBucketsAndMask(newBuckets, newCapacity - 1);
/// 释放旧桶子,也就是把原来的缓存数据丢掉不要了
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
#if CACHE_END_MARKER
bucket_t *cache_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.
/// 申请内存空间,大小:newCapacity
bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
/// 创建桶子最后一个元素,来标记buckets末尾
bucket_t *end = endMarker(newBuckets, newCapacity);
/// 官方注释也写的很明白,把最后一个元素设置上值,sel是1,
#if __arm__
// End marker's sel is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
// End marker's sel is 1 and imp points to the first bucket.
end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
/// 打印相关信息
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
#else
/// 真机会走这个方法,直接申请内存,没有插入endBucket
bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
if (PrintCaches) recordNewCache(newCapacity);
return (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
}
#endif
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
/// 从这个方法代码可以看出_bucketsAndMaybeMask是存buckets。
/// 这里有个小细节newMask是(newCapacity - 1),也就是总长度减1
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;
}
/// 这里又有小细节,mac环境mask直接用_maybeMask,和真机不一样哦,调试时会发现
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 || CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
/// 真机会走这里~~
/// 把mask和buckets存到_bucketsAndMaybeMask里面,基于位运算
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
uintptr_t buckets = (uintptr_t)newBuckets;
uintptr_t mask = (uintptr_t)newMask;
ASSERT(buckets <= bucketsMask);
ASSERT(mask <= maxMask);
_bucketsAndMaybeMask.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, memory_order_relaxed);
_occupied = 0;
}
/// 真机的mask是根据_bucketsAndMaybeMask来的,这也是写类似结构体真机调试时_maybeMask一直为0的原因
mask_t cache_t::mask() const
{
uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);
return maskAndBuckets >> maskShift;
}
/// 真机是每次-1,当减到0时,设置为mask
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
上面代码关键内容我都进行了注释,里面有些需要注意的细节
- mac初始创建容量为4,真机环境初始创建容量为2,每次扩容是当前容量的2倍
- mac当_occupied+1大于3/4容量时进行扩容,为什么是3/4呢?因为负载因子0.75时,空间利用率是比较好的,和对hash冲突可以有效避免,具体的可查阅相关算法知识
- 真机当容量小于等于8的时候是可以填满
buckets
的,大于8时负载因子是7/8 - _maybeMask的值是容量减1
- mac环境_maybeMask直接可以读到值,真机时没有,只能用
_bucketsAndMaybeMask
做位运算获取 - hash算法解决冲突时,
arm64
与非arm64
算法不同,真机是减1,减到0是等于mask
- buckets是散列表结构,单是注意不是数组!不是数组!!不是数组!!!强调3遍!!!
- 每次扩容申请新buckets时,都会把旧的释放掉
- mac环境buckets有个结束标志,新建时把最后一个
bucket
设置到buckets
末尾,来标记这段散列表的结束,它的sel
是1。真机是没有结束标志的。
insert
流程我们总结完了,是不是对cache_t
的理解更深一步呢?什么??上面代码太乱???好吧,我就总结下insert
的流程图吧,因为mac和真机流程不一样,所以这里总结两个流程图:
insert
流程总结完,我们看一个非常有意思的现象。创建SJPerson
类,里面生命并实现say1
、say2
方法:
@interface SJPerson : NSObject
- (void)say1;
- (void)say2;
@end
@implementation SJPerson
- (void)say1
{
NSLog(@"%s", __func__);
}
- (void)say2
{
NSLog(@"%s", __func__);
}
@end
然后我们执行下面代码并打断点
SJPerson *p = [SJPerson alloc];
[p say1];
执行完say1
的时候,按上面的分析,maybeMask
应该等于3,lldb进行调试验证:
验证没有问题,我们增加代码[p say2]
,并在say2
后面打断点,按上面的分析,maybeMask
应该等于3,lldb进行调试验证:
验证通过,接下来我们做一个骚操作,删除say2
,在say1
执行完后打断点,这时候我们已经验证过maybeMask
是3,occupied
是1,没有问题。这时我们在lldb
中输入p [p say2]
,这时候也是让p
执行say2
方法,按道理执行完cache
里面的结果应该跟上面一样,进行验证:
神奇的一幕出现了,maybeMask
成7了,occupied
还是2没问题。诶?7也就是说cache
做了一次扩容,但是我们就执行了两个方法,初始容量是4,负载因子0.75,2小于3应该不会扩容啊,我们继续看这8个位置的方法都有什么。
缓存里面有class
和say2
两个方法,但是class我们并没有执行啊,这时候我们猜测在lldb
调试执行p [p say2]
,系统会自动执行一些方法。在insert
方法里面打印相关信息printf("cache_t insert ====== sel : %s imp : %p receiver : %p \n", (char *)sel, imp, receiver);
我们看到p [p say2]
时,插入了很多方法,我们只看receiver
是p
对象的方法,有三个,这时候我们可以继续分析,[p say1]
时候插入一个方法;要插入respondsToSelector
时,计算3<=43/4,继续插入;插入class
时,4>43/4,所以要扩容重新开辟内存,这是容量就变成8了,maybeMask
为7;插入say2
时,3<=8*3/4,插入,这时cache
里面有两个方法,跟我们分析的一致了。
真机验证insert
我们先根据上面分析猜测一下真机验证的结果。我们画个猜测流程图:
真机cache缓存流程猜测.jpg流程猜测完了,真机验证下是否和我们猜测结果一致呢?答案是一致,非常完美,附上测试demo的链接,可以下载测试。cache_t-Demo
这里还有个坑点,我最早跑demo时候,结果和上面流程并不一样,初始容量4,负载因子一致是0.75,把源码看了一遍又一遍都怀疑人生了。后来发现是自己iOS
版本太低,因为objc源码
我下载最新的,但是iOS
版本低,真机运行和分析结果会有所差别,所以在分析时,
一定要下载最新的源码,iOS
升级最新系统,xcode升级最新版本!!!
一定要下载最新的源码,iOS
升级最新系统,xcode升级最新版本!!!!!
一定要下载最新的源码,iOS
升级最新系统,xcode升级最新版本!!!!!!!
重要的事情说3遍,我下载源码版本818.2,手机iphoneX,iOS14.6,xcode12.5。
objc_msgSend引入
缓存的插入我们已经了解了,那在什么时机会调用插入呢,我们继续研究。
在objc-cache.mm
可以看到文件说明注释中写的很清楚
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
*
* Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
* cache_t::copyCacheNolock (caller must hold the lock)
* cache_t::eraseNolock (caller must hold the lock)
* cache_t::collectNolock (caller must hold the lock)
* cache_t::insert (acquires lock)
* cache_t::destroy (acquires lock)
在读缓存的时候,最早会调用objc_msgSend
,也就是发送消息时候会进行读取方法。下一个篇章分析objc_msgSend
。