1.cache_t的源码分析
之前我们探索了类的claas_data_bits_t
,今天来分析下cache_t
,先打印下LGPerson
(lldb) x/4gx LGPerson.class
0x100008408: 0x0000000100008430 0x000000010036a140
0x100008418: 0x0000000101b1bd40 0x0002802800000003
(lldb) p/x (cache_t *)0x0000000100008440
(cache_t *) $1 = 0x0000000100008440
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298515376
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32808
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802800000000
}
}
}
}
进入源码:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
};
我们对缓存操作肯定是对属性或者方法进行存储和读取,思路是查找cache_t
对应的方法
void insert(SEL sel, IMP imp, id receiver);
|
void cache_t::insert(SEL sel, IMP imp, id receiver)
{。。。。
bucket_t *b = buckets();
mask_t m = capacity - 1; // 4-1=3
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)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
}
中间关键是对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
。。。
}
这里说明下:架构(真机) : arm64; 模拟器 : i386; mac : x86_64
__LP64__等一些宏定义的意义
继续之前的探索,我们之前探索class 中的bits 时候去找属性列表,方法列表都有对应的方法,我们继续查找
cache_t
发现它有buckets()
方法 返回的是 bucket_t *
,里面包含了_imp
和_sel
。继续打印调试
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x00000001003623b0
(lldb) p *$3
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
是空的没有缓存的方法,我们调用一下方法:
p [p saySomething]
2021-06-23 14:55:25.722111+0800 KCObjcBuild[50955:1160095] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4301690528
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802800000007
}
}
}
}
_occupied
变成了1,代表已占用的标识。_maybeMask
为7,发生了改变。我们继续打印
p $3.buckets()
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
还是空的为什么?去源码中看 buckets()
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
buckets
看起来是一个数组,复数s,这里说下数组和链表的概念
数组:
- 在内存中,数组是一块连续的区域。
- 数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。
- 插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。
- 随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。
- 并且不利于扩展,数组定义的空间不够时要重新定义数组。
链表:
- 在内存中可以存在任何地方,不要求连续。
- 每一个数据都保存了下一个数据的内存地址,通过这个地址找到下一个数据。 第一个人知道第二个人的座位号,第二个人知道第三个人的座位号……
- 增加数据和删除数据很容易。 再来个人可以随便坐,比如来了个人要做到第三个位置,那他只需要把自己的位置告诉第二个人,然后问第二个人拿到原来第三个人的位置就行了。其他人都不用动。
- 查找数据时效率低,因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。 要找到第三个人,必须从第一个人开始问起。
- 不指定大小,扩展方便。链表大小不用定义,数据随意增删。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素
buckets
就是这样的方式进行存储 继续打印首位置进行内存平移
(lldb) p $3.buckets()[1]
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 49064
}
}
}
(lldb) p $4.sel()
(SEL) $7 = "saySomething"
(lldb) p $4.imp(nil,pClass)
(IMP) $8 = 0x0000000100003be0 (KCObjcBuild`-[LGPerson saySomething])
就是我们想要的结果 继续打印:
lldb) p/x pClass
(Class) $0 = 0x0000000100008448 LGPerson
(lldb) p (cache_t*)0x0000000100008458
(cache_t *) $1 = 0x0000000100008458
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298515376
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32808
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802800000000
}
}
}
}
(lldb) p [p saySomething]
2021-06-23 15:43:43.577791+0800 KCObjcBuild[51131:1181293] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4313870720
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802800000007
}
}
}
}
(lldb) p [p saySomething]
2021-06-23 15:43:59.324172+0800 KCObjcBuild[51131:1181293] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $4 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4313870720
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32808
_occupied = 3
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0003802800000007
}
}
}
}
(lldb) p [p saySomething]
2021-06-23 15:45:04.046302+0800 KCObjcBuild[51131:1181293] -[LGPerson saySomething]
(lldb) p *$1
(cache_t) $5 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4313870720
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32808
_occupied = 3
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0003802800000007
}
}
}
}
开始么有调用方法的时候_occupied
为0,调用一次后_occupied
为1,再次调用后_occupied
为3,在之后调用不发生改变。探究下缓存的过程做了什么
2.cache_t的inset流程
我们第一次调用方法,所以没有缓存,会直接走入方法查找lookUpImpOrForward
方法中接着会进入log_and_fill_cache
,然后cache_fill
,在cache_fill
方法中我们找到了cache->insert
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
mask_t newOccupied = occupied() + 1; // 1+1累加每次进来+1
unsigned oldCapacity = capacity(), capacity = oldCapacity;计算容量。现在是否有空间有的+1,没有的话是0
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//4
//创建4个容量的buckets
reallocate(oldCapacity, capacity, /* freeOld */false);
}
//CACHE_END_MARKER为1,在真机下arm64值为0.新的newOccupied+1(或者0)是否是之前开辟空间的小于等于75%是的话不用开辟了,
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 {// 4*2 = 8,是否有,有的话扩容*2,么有就是4
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;//是否超过最大,超过就按最大的32来
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1; // 4-1=3
mask_t begin = cache_hash(sel, m);//用mask进行哈希
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)) {
incrementOccupied();// _occupied++;累加,开辟新空间的时候自动_occupied = 0并释放老的里面存储空间内容
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));//cache_next进行循环查找
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
insert
方法可以得知cache在一开始没有的话会分配了4
个容量,在arm
、i386
(模拟器)、x86_64
(mac)下需要特殊的结束标记存储区进行换行,所以CACHE_END_MARKER
为1,arm64
下不需要结束标记故为0,在newOccupied + 0 <= capacity / 4 * 3
,大于时如果capacity有值就按2倍扩容,为空就设置为4。
还可以看到通过buckets
中的根据i
找到sel
与0
对比来查找buckets中的空位置
,找到后就把occupied
加1,未使用的插槽并插入那里。存在的话就不需要处理了。
这里有个cache_next
, 在arm64下是通过i-1
来得出i, 而在arm、i386、x86_64下是通过(i + 1) & mask
得出的i。
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);//mask_t和newCapacity关联减1
setBucketsAndMask(newBuckets, newCapacity - 1);//把newBuckets和mask_t进行关联 _occupied =0
if (freeOld) {
collect_free(oldBuckets, oldCapacity);//释放老的
}
}
allocateBuckets
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.
bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
bucket_t *end = endMarker(newBuckets, newCapacity);//进行标记的
#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);//__arm__ 结束标记的sel是1,imp点在第一个桶之前。
#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);结束标记的sel是1,imp点在第一个桶
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
allocateBuckets
方法在CACHE_END_MARKER
为1才会进行end标记,否则直接开辟空间,真机的情况不会进行标记。
3.自定义方法,探究
我们开发中可以模仿系统的写法,重写或者自定义
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct kb_bucket{
SEL _sel;
IMP _imp;
};
struct kb_cache_t{
struct kb_bucket *buckets;//8
mask_t mask;//4
uint16_t flags; //2
uint16_t _occpied;//2
};
struct kb_bits
{
uintptr_t bits;
};
struct kb_objc_class
{
Class isa;
Class superClass;
struct kb_cache_t cache_t;
struct kb_bits bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
KBPerson *kb = [KBPerson alloc];
[kb sayNB1];
[kb sayNB2];
[kb sayNB3];
// [kb sayNB4];
// [kb sayNB1];
// [kb sayNB2];
// [kb sayNB3];
struct kb_objc_class *kb_class =(__bridge struct kb_objc_class*)(KBPerson.class);
struct kb_cache_t kb_cache = kb_class->cache_t;
NSLog(@"%u-%u",kb_cache._occpied,kb_cache.mask);
for (mask_t i=0; i<kb_cache.mask; i++) {
struct kb_bucket bucket = kb_cache.buckets[i];
NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"Hello, World!");
}
return 0;
}
运行结果: 没有经过lldb调试也验证了cache_t的变化
2021-06-24 13:51:36.250227+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB1]
2021-06-24 13:51:36.250663+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB2]
2021-06-24 13:51:36.250703+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB3]
2021-06-24 13:51:36.250728+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB4]
2021-06-24 13:51:36.250750+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB1]
2021-06-24 13:51:36.250771+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB2]
2021-06-24 13:51:36.250793+0800 自定义Cache_t分析[58217:1606919] -[KBPerson sayNB3]
2021-06-24 13:51:36.250817+0800 自定义Cache_t分析[58217:1606919] 4-7
2021-06-24 13:51:36.251113+0800 自定义Cache_t分析[58217:1606919] sayNB1 - 0xbd20f
2021-06-24 13:51:36.251170+0800 自定义Cache_t分析[58217:1606919] sayNB2 - 0xbcf0f
2021-06-24 13:51:36.251206+0800 自定义Cache_t分析[58217:1606919] (null) - 0x0f
2021-06-24 13:51:36.251255+0800 自定义Cache_t分析[58217:1606919] (null) - 0x0f
2021-06-24 13:51:36.251280+0800 自定义Cache_t分析[58217:1606919] (null) - 0x0f
2021-06-24 13:51:36.614742+0800 自定义Cache_t分析[58217:1606919] sayNB4 - 0xbc50f
2021-06-24 13:51:36.614781+0800 自定义Cache_t分析[58217:1606919] sayNB3 - 0xbc80f
_maybeMask
代表可用的空间字节,_occupied
已用空间字节,buckets
是一个哈希表通过内存平移得到对应的bucket
,bucket
里面存放了对应的SEL
和IMP
,存入的时候第一次开辟一个4字节的内存存入bucket,之后判断是否是_occupied +1 +CACHE_END_MARKER 是否小于等于当前空间的75%(#if arm || x86_64 || i386
#define CACHE_END_MARKER 1
#elif arm64 && !LP64
#define CACHE_END_MARKER 0)不小于就当前capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE
扩容2倍,同是释放以前存储的buckets
,_occupied =0
之后_occupied
继续累加
4.cache_t的补充
- 之前我们在lldb调试的时候
_bucketsAndMaybeMask
的Value是有值的,官方说_bucketsAndMaybeMask is a buckets_t pointer
是一个buckets_t
指针,所以
和
buckets()
一样,之后指针平移得到对应的bucket_t
。
bucket_t
中sel
和imp
的读取 sel:
imp:
- 类方法的调用 之前我们知道类方法存在元类中,那么类类方法的缓存是不是也在里面验证下
- 最后写个大概的流程图