前言
在之前的探索中,我们探索了类的结构,从源码的角度我们可以看到
其中的Class isa
、Class superclass
、class_data_bits_t bits
我们在之前已经进行过分析,今天我们就来探索下剩下的这个cache_t cache
。
cache_t的结构初探
cache_t cache
顾名思义,是用来存放缓存的地方。那么他到底缓存的是什么东西呢?继续从源码的角度开始我们的探索
我们可以看到,cache_t
是一个结构体,其中包含一个uintptr_t
泛型类型的_bucketsAndMaybeMask
和一个联合体,联合体中又包含了一个结构体和一个preopt_cache_t *
泛型类型的_originalPreoptCache
。
_bucketsAndMaybeMask
从源码中我们可以看到,_bucketsAndMaybeMask
是一个uintptr_t
泛型类型的数据结构,我们可以简单里的理解为就是uintptr_t
类型的,而我们看下uintptr_t
的定义
#ifndef _UINTPTR_T
#define _UINTPTR_T
typedef unsigned long uintptr_t;
#endif /* _UINTPTR_T */
复制代码
实际上我们可以粗略的认为这就是一串数字,而这串数字里面存储了什么呢?继续看下面的源码
在不同的架构里面,_bucketsAndMaybeMask
存储的东西并不一样,在 CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
时,_bucketsAndMaybeMask
实际上就是一个buckets_t
的指针,而在CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
时,_bucketsAndMaybeMask
中存在一个混合的数据,其中低48位存储的是buckets_t
的指针,而高16位存储的是_maybeMask
的值。
1. 架构区分
看到这么多的if-else
我们就想去探索下他们所指的到底是什么,在源码中查找后,原来他们代表不同的机器架构,具体如下
2.不同架构下的_bucketsAndMaybeMask
在源码中,我们看到了在不同的架构下,_bucketsAndMaybeMask
中存储的东西并不一样,他们之间的关系,我们可以粗略的以下图概括
buket_t(简称筒子)
我们在上面的_bucketsAndMaybeMask
结构中发现,他最主要的作用是存储buckets
,那么这个buckets
又是什么东西呢?查找源码发现
实际上是一个指向bucket_t
的指针,再来看看bucket_t
我们惊喜的发现了我们熟悉的SEL
和imp
的存在。
LLDB探索cache_t
从源码的层面上,我们基本上找到了我们想要的东西,也就是cache_t里面存储的SEL
和imp
。下面我们从LLDB
的角度来继续探索,继续掏出我们的DMPeron
并在方法调用前,分别打上断点,然后我们就要开始咯~
断点分析
- 首先我们进入第一个断点,这个时候我们什么方法都还没开始调用
- 接下来我们进入第二个断点,这个时候
[p1 say1]
、[p1 say2]
都调用完毕,继续来看看内存中的数据
调用了两个方法,发现_occupied = 2
、_maybeMask = 3
(这里我用的是inter芯片的电脑)。然后继续去获取buckets
,在首地址也就是0号位置,我们打印出了空,因为是buckets
所以,继续往后获取1号和2号位置,我们找到了我们调用的方法say1
、say2
。
- 再看第三个断点,这时候
[p1 say1]
、[p1 say2]
、[p1 say3]
、[p1 say4]
都已经调用完毕,猜想我们能在buckets
里面找到他们,继续调试
我们发现,并没有找到我们的say1
和say2
,并且_maybeMask = 7
、_occupied
还是等于2
。
问题汇总
在上面的实验中,我们发现了几个问题
_maybeMask
和_occupied
的作用是什么?- 方法的存储为什么不是顺序的?
- 为什么调用
say3
、say4
后就知道不到say1
、say2
了?
cache_t结构再探
通过查看LLDB
中cache_t
所存储的内容,我们找到了一些问题,下面我们就带着问题,继续去源码中寻找答案,既然这些问题都与cache_t
结构有关,那么我们就去里面寻找我们想要的答案。cacht_t
我们知道这是一个缓存,那么既然是缓存,肯定是要读取和写入
的,我们来到源码中开始寻找相关的方法。
insert方法
在源码中,我们找到了一个叫做insert
的方法,接受的参数分别是sel
、imp
、receiver
。看起来似乎这就是我们要寻找的写入缓存的方法,看看他的实现
初始化容器
方法比较长,我们先看前面部分,进到方法后,先获取occupied
并且给他加1
,因为我们是在工程里面首次调用DMPerson
的方法,因此occupied = 0
,生成的newOccupied = 1
。接下来获取capacity
,我们点进去这个方法看下
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
复制代码
发现实际上就是获取_maybeMask
的值,如果不为0
,则+1
。同样因为之前没有调用方法,所以这里oldCapacity = 0
,并且在下面的if-else
中会执行reallocate
方法中,其中capacity
是一个常量,我们可以看到INIT_CACHE_SIZE = 4
。再看看reallocate
方法
很明显,这个方法的作用是初始化一个新的buckets
并且在freeOld = true
的时候释放旧的buckets
。最重要的方法就是setBucketsAndMask
,继续往里面追踪,
由于使用的是inter芯片的mac电脑,所以只看红框部分,这里是将newBuckets
设置进了_bucketsAndMaybeMask
,同时将newMask
也就是4-1 = 3
设置进了_maybeMask
,_occupied = 0
。很明显这一部分内容就是将我们创建的容器进行初始化。这里也对我们上面问题1进行了解答,_occupied
实际上就是cache
中缓存方法的个数,而_maybeMask
就是缓存容器的最大容量。
数据插入
继续看下面的代码,初始化完容器后,自然是要进行插入流程
首先先获取了我们初始化的buckets
的首地址指针。接着通过cache_hash
函数来计算出我们的数据需要插入的初始下标,在这里我们算出来的begin = 1
其实就是一个算法mask ^ sel
。算出初始下标后,进入下面的do-while
循环中。
- 获取
buckets
中对应下标位置的内存空间,看是否有sel占用位置,如果没有,则直接将数据插入到该位置,并且_occupied
的值加1
- 如果存在,这直接进入
while
的条件,i
通过cache_next
函数进行再哈希,并且算出的结果不等于bengin
循环继续。
我们再来看看cache_next
函数
如果是我们目前的inter芯片的mac电脑环境,则直接i+1 &mask
,如果是真机环境,则是i ? i-1 : mask
。这里就解释了我们的问题2,因为是通过哈希函数进行下标计算存储,所以存储的数据是无需且不连续的。
扩容
我们say1
方法的流程至此已经全部走完,可以看到容器的最大存储个数是3个,所以当存储空间不够的时候,必然会出现扩容的存在,因此我们看看再次插入的时候,会出现什么情况
我们可以看到,当新插入的数据满足某个条件的时候,则什么也不做,直接进行插入,否则容量会进行二倍的扩容,那么这个条件,可以看到是这样的
- 在inter芯片的mac电脑上,
newOccupied + 1
小于等于满容量的3/4
时,不进行扩容。- 在真机/M1电脑的环境下,
newOccupied
小于等于满容量的7/8
时,不进行扩容。
我们使用的是inter芯片的mac电脑,所以当插入第三个数据,也就是say3
的时候,会进行扩容处理。
可以看到,扩容处理实际上就是新建了一片新的内存空间,容量是之前的两倍,并且将之前旧的内存空间释放,所以问题3的答案我们也找到了,当调用say3
时,会进行扩容处理,并将之前的缓存给释放掉,所以找不到say1
、say2
方法的缓存了。
cache_t结构总结
我们通过源码和LLDB代码调试相结合,基本上将cache_t
的结构给探索明白了
_bucketsAndMaybeMask
的作用主要是存放buckets
也就是我们的存储容器的地址,同时在不同的架构中,还可能会存放_maybeMask
这个数据。_maybeMask
是我们容器的最大可存储值_occupied
是缓存中缓存的方法的数量
同时我们还研究了一下数据插入的流程,当插入的数据大于满容量的3/4(真机是7/8)
时,会对容器进行扩容处理,并且将旧的缓存给释放掉。在这里我们只研究了插入的过程,那么读取的过程,我们在下个篇章继续探索。