iOS探索底层-cahce_t初探

前言

在之前的探索中,我们探索了类的结构,从源码的角度我们可以看到

image.png
其中的Class isaClass superclassclass_data_bits_t bits我们在之前已经进行过分析,今天我们就来探索下剩下的这个cache_t cache

cache_t的结构初探

cache_t cache顾名思义,是用来存放缓存的地方。那么他到底缓存的是什么东西呢?继续从源码的角度开始我们的探索

image.png
我们可以看到,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 */
复制代码

实际上我们可以粗略的认为这就是一串数字,而这串数字里面存储了什么呢?继续看下面的源码

image.png
在不同的架构里面,_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我们就想去探索下他们所指的到底是什么,在源码中查找后,原来他们代表不同的机器架构,具体如下
image.png

2.不同架构下的_bucketsAndMaybeMask

在源码中,我们看到了在不同的架构下,_bucketsAndMaybeMask中存储的东西并不一样,他们之间的关系,我们可以粗略的以下图概括
image.png

buket_t(简称筒子)

我们在上面的_bucketsAndMaybeMask结构中发现,他最主要的作用是存储buckets,那么这个buckets又是什么东西呢?查找源码发现

image.png
实际上是一个指向bucket_t的指针,再来看看bucket_t

image.png
我们惊喜的发现了我们熟悉的SELimp的存在。

LLDB探索cache_t

从源码的层面上,我们基本上找到了我们想要的东西,也就是cache_t里面存储的SELimp。下面我们从LLDB的角度来继续探索,继续掏出我们的DMPeron

image.png
并在方法调用前,分别打上断点,然后我们就要开始咯~

断点分析

  • 首先我们进入第一个断点,这个时候我们什么方法都还没开始调用

image.png

  • 接下来我们进入第二个断点,这个时候[p1 say1][p1 say2]都调用完毕,继续来看看内存中的数据

image.png
调用了两个方法,发现_occupied = 2_maybeMask = 3(这里我用的是inter芯片的电脑)。然后继续去获取buckets,在首地址也就是0号位置,我们打印出了空,因为是buckets所以,继续往后获取1号和2号位置,我们找到了我们调用的方法say1say2

  • 再看第三个断点,这时候[p1 say1][p1 say2][p1 say3][p1 say4]都已经调用完毕,猜想我们能在buckets里面找到他们,继续调试

image.png
我们发现,并没有找到我们的say1say2,并且_maybeMask = 7_occupied还是等于2

问题汇总

在上面的实验中,我们发现了几个问题

  1. _maybeMask_occupied的作用是什么?
  2. 方法的存储为什么不是顺序的?
  3. 为什么调用say3say4后就知道不到say1say2了?

cache_t结构再探

通过查看LLDBcache_t所存储的内容,我们找到了一些问题,下面我们就带着问题,继续去源码中寻找答案,既然这些问题都与cache_t结构有关,那么我们就去里面寻找我们想要的答案cacht_t我们知道这是一个缓存,那么既然是缓存,肯定是要读取和写入的,我们来到源码中开始寻找相关的方法。

insert方法

image.png
在源码中,我们找到了一个叫做insert的方法,接受的参数分别是selimpreceiver。看起来似乎这就是我们要寻找的写入缓存的方法,看看他的实现

初始化容器

image.png
方法比较长,我们先看前面部分,进到方法后,先获取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方法

image.png
很明显,这个方法的作用是初始化一个新的buckets并且在freeOld = true的时候释放旧的buckets。最重要的方法就是setBucketsAndMask,继续往里面追踪,

image.png
由于使用的是inter芯片的mac电脑,所以只看红框部分,这里是将newBuckets设置进了_bucketsAndMaybeMask,同时将newMask也就是4-1 = 3设置进了_maybeMask_occupied = 0很明显这一部分内容就是将我们创建的容器进行初始化。这里也对我们上面问题1进行了解答,_occupied实际上就是cache中缓存方法的个数,而_maybeMask就是缓存容器的最大容量

数据插入

继续看下面的代码,初始化完容器后,自然是要进行插入流程
image.png
首先先获取了我们初始化的buckets的首地址指针。接着通过cache_hash函数来计算出我们的数据需要插入的初始下标,在这里我们算出来的begin = 1
image.png
其实就是一个算法mask ^ sel。算出初始下标后,进入下面的do-while循环中。

  1. 获取buckets中对应下标位置的内存空间,看是否有sel占用位置,如果没有,则直接将数据插入到该位置,并且_occupied的值加1
  2. 如果存在,这直接进入while的条件,i通过cache_next函数进行再哈希,并且算出的结果不等于bengin循环继续。

我们再来看看cache_next函数

image.png
如果是我们目前的inter芯片的mac电脑环境,则直接i+1 &mask,如果是真机环境,则是i ? i-1 : mask。这里就解释了我们的问题2,因为是通过哈希函数进行下标计算存储,所以存储的数据是无需且不连续的

扩容

我们say1方法的流程至此已经全部走完,可以看到容器的最大存储个数是3个,所以当存储空间不够的时候,必然会出现扩容的存在,因此我们看看再次插入的时候,会出现什么情况

image.png
我们可以看到,当新插入的数据满足某个条件的时候,则什么也不做,直接进行插入,否则容量会进行二倍的扩容,那么这个条件,可以看到是这样的

image.png

  • 在inter芯片的mac电脑上,newOccupied + 1小于等于满容量的3/4时,不进行扩容。
  • 在真机/M1电脑的环境下,newOccupied 小于等于满容量的7/8时,不进行扩容。

我们使用的是inter芯片的mac电脑,所以当插入第三个数据,也就是say3的时候,会进行扩容处理。

image.png
可以看到,扩容处理实际上就是新建了一片新的内存空间,容量是之前的两倍,并且将之前旧的内存空间释放,所以问题3的答案我们也找到了,当调用say3时,会进行扩容处理,并将之前的缓存给释放掉,所以找不到say1say2方法的缓存了

cache_t结构总结

我们通过源码和LLDB代码调试相结合,基本上将cache_t的结构给探索明白了

  • _bucketsAndMaybeMask的作用主要是存放buckets也就是我们的存储容器的地址,同时在不同的架构中,还可能会存放_maybeMask这个数据。
  • _maybeMask是我们容器的最大可存储值
  • _occupied是缓存中缓存的方法的数量

同时我们还研究了一下数据插入的流程,当插入的数据大于满容量的3/4(真机是7/8)时,会对容器进行扩容处理,并且将旧的缓存给释放掉。在这里我们只研究了插入的过程,那么读取的过程,我们在下个篇章继续探索。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享