前言
在前面几个章节中我们知道了类的底层结构,包含isa,superClass,cache,bits,并相继学习isa走位图,类的继承链,存储了属性,方法,协议等信息的bits,除了这些,类里面还有一个重要的变量,那就是cache,按照字面意思来说是缓存,那它到底是个什么东西呢?又做了些什么事情呢?接下来我们一起进入到cache原理的学习。
cache的认识
就cache本身的字面意思来说,我们知道是缓存,而其作为类的一个变量,那它到底是缓存了什么信息呢?首先我们还是回到objc源码,找到类的实现:
cache_t结构
我们知道了cache是cache_t类型,那进入到cache_t去瞧一瞧:
可以看到
cache_t也是一个结构体,有很多的方法和static修饰的变量,主要的变量有_bucketsAndMaybeMask变量和一个联合体;
-
explicit_atomic<uintptr_t> _bucketsAndMaybeMask:是一个泛型的结构体,真正的大小由uintptr_t(无符号长整型,占用8字节)决定,占用8字节; -
联合体
union:里面只有一个变量一个结构体,-
这个结构体中有3个变量:占用
8字节_maybeMask:一个mask_t(uint32_t)型的结构体 占用4个字节_flags:uint16_t类型 占用2字节_occupied:uint16_t类型 占用2字节
-
_originalPreoptCache:是一个结构体指针类型,占用8字节
-
而联合体是里面是互斥的,那么可以得到联合体大小为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;
}
复制代码
所以通过上面分析我们可以得到cache_t占用16个字节。
注意架构宏判断
我们往下看会发现很多在不同架构下对 _bucketsAndMaybeMask的说明:
//_bucketsAndMaybeMask is a buckets_t pointer
//_maybeMask is the buckets mask
_bucketsAndMaybeMask确切的说,不单单是一个bucket_t的指针地址,而是一个掩码,包含了首个bucket_t的地址和mask(针对真机环境);继续后面看又能发现很多bucket_t的代码,那bucket_t又是什么呢?
从上面的代码我们大概可以看到初始化bucket_t等一些操作,那更加证明cache_t主要就是围绕着bucket_t来做了一些操作。
bucket_t结构
果不其然,通过
bucket_t结构我们发现
bucket_t结构里面有_sel和_imp两个变量(注意这边有个架构的区分,__arm64__和其他,不过都是_sel和_imp,只不过顺序不同);- 可以确定
bucket_t就是对方法操作;进一步确定了cache_t就是来缓存方法的。 那所得结论是否正确呢?下面通过LLDB调试一下看看。
LLDB验证方法的存储
还是那个LhkhPerson类,然后我们通过断点进入到LLDB;前面我们探索过bits,那么我们这边也类似(cache_t是首地址偏移16个字节,也需要强转为cache_t类型),取到cache_t内部变量,然后通过指针地址取到数据,结果给我们的是error,error,error。。。什么鬼?
是哪里错了吗?我们回想一下,探索
bits的时候,我们是通过查找到class_rw_t其内部methods(),properties()等最终获取到我们想要的数据,那cache_t呢?别废话了,一头扎进源码吧:
上面探究的时候我们就已经知道
cache_t是围绕bucket_t来的,那这边我们发现了buckets(),直接取出来看看呢:
有地址了额,看来方向是对的,然后取出里面的数据,结果呢
_sel里面value=nil,_imp里面value=0,这啥玩意儿啊?回过头一想,既然是缓存方法的,那肯定得调用方法才行啊,我们这都没有调用方法怎么可能有缓存呢。。。好吧,调用一下我们LhkhPerson里面声明的方法:
方法调用了,那我们重新来打印一次
发现还是
nil和0,但是我们发现_occupied相比之前值变了,既然值变了,那说明调用方法过后有变化,buckets()看着又像是一个数组,那我们试着取一下数组里面的值呢
我们再去探究一下bucket_t:
我们发现sel()和imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls),通过sel()打印一下看看呢:
打印出了我们的方法
sel是saySomething,对应imp是0x0000000100003c90 (KCObjcBuild-[LhkhPerson saySomething])
我们可以多调用几个方法看看呢:
这个是没有调用方法的时候,_maybeMask的value = 0,_occupied = 0
我们调用第一个方法
saySomething后,_maybeMask的value = 7,_occupied值变成1了(问题点1)
和上面一样,我们取了很多次才找到(问题点2),打印出来方法名确定是我们的saySomething
那继续调用方法sayHello,_maybeMask的value = 7没变,_occupied值变成4了
经过这一步后我们找到了找到了sayHello,继而也找到了saySomething,那么我们继续调用第三个方法sayNB,_maybeMask的value = 7没变,_occupied 值变成5了
三个方法都找到了,继续验证当我们调用7个方法时,_maybeMask的value = 15,_occupied 值变成6(我们调用了7个方法,为什么是6呢)
我们找到了say666、say777、say999三个方法,然后其他4个方法往后取了20多个数据没有找到,那其余方法呢?(问题点3)
总结
bucket_t里面存的是方法的sel和impcache_t中存的是很多的bucket_t
使用LLDB探究时,我们肯定会发现:
- 每当多调用一个方法过后
_occupied和_mask会发生改变这个是为什么呢? - 读取
buckets里面的数据时,可以发现里面缓存的方法并不是有序的,而是无序的,这又是为什么呢? - 当调用很多不同的方法过后,我们在后面取方法数据时发现有方法已经找不到了,这又是什么情况?
代码转换探索
上面我们使用LLDB调试分析cache,每次调用以此方法后重新获取数据,过程还是有点繁琐的,那有没有更方便直接的方法呢?那肯定是有的啊,我们通过自定义源码的一些结构体实现,来脱离源码分析:
我们的cache是在class里面,那么我们来自己写一个class的结构体出来:
参照底层源码我们自定义的结构体,然后下面我们就使用一下:
调用方法都打印出来了,这样就使得我们脱离源码也是可以得到上面lldb相同的结果:
_maybeMask和_occupied两者的值随着方法的调用会改变;- 方法符号取出来无序性;
- 方法丢失。 既然结果是相同的,那么我们就只有通过源码来探究这几个问题了。
cache底层探索
既然cache是在缓存方法,那么要想缓存那必须得将方法插入到cache_t表中啊,所以肯定有insert方法,那么带着这个思路我们到cache_t中去看看:
cache_t::insert分析
可以看到在cache_t中可以查找到插入的方法,并且传入了三个参数,sel方法符号,imp地址指针,receiver消息接受者;进入到方法实现中:
这部分源码里面包含的信息量还是很大的,也调用了不少的方法,也有不少的逻辑判断,我们一起详细分解一下:
容器空间创建
除去一些无关紧要的代码,我们知道因为是插入方法,那么刚开始时肯定是空的,那么我们肯定就是找到刚开始为空的时候做了些什么:
mask_t newOccupied = occupied() + 1; // 刚开始occupied()肯定为0,也就是调用一个方法加一次1
unsigned oldCapacity = capacity(), capacity = oldCapacity;//取到这个容量,相当于初始化
if(slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
if (!capacity) capacity = INIT_CACHE_SIZE;判断是否为空,开始的时候肯定是空,这个时候给他赋值INIT_CACHE_SIZE-
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),INIT_CACHE_SIZE_LOG2 = 2,所以也就是1左移两位也就是4
-
reallocate(oldCapacity, capacity, /* freeOld */false);然后就是调用这个reallocate方法创建容器分配容量,这个方法内部又调用了很多的方法.
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);//设置mask和bucket
if (freeOld) {//释放掉老的容器空间取决于freeOld
collect_free(oldBuckets, oldCapacity);
}
}
allocateBuckets新容器创建和开辟内存空间;
setBucketsAndMask设置mask和bucket,并设置_occupied=0;
3.
collect_free释放老容器,回收内存;取决于freeOld参数。
capacity容量:
- 第一次进入缓存时
4-1也就是3; - 小于3/4容量和小于总容量时啥也没干;
- 扩容:
//capacity是否有值判断,有则两倍扩容,否则就是4(1<<2)
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
//判断capacity是否大于最大值2^15,大于则直接等于2^15,所以最大扩容为2^15
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
//重新创建容器,开辟空间,freeOld=true 释放老容器,回收内存
reallocate(oldCapacity, capacity, true);
capacity容量验证:
第一次调用1个方法时,首次进入时occupied=1,capacity=4;
当我们调用了3个方法时
调用4个方法时
通过源码调试我们验证了扩容的操作,但是也给我带来了新问题,为什么调用了4个方法,但是
occupied=2呢?我们将这个联系到问题点3,猜想难道是方法丢失?
方法缓存
可以看出来,方法缓存时通过哈希表,我们可以先脑补一下数组,链表和哈希表:
数组,链表,哈希表补充
-
数组:数组是用于储存多个相同类型数据的集合。主要有以下优缺点:
- 优点:访问某个下标的内容很方便,速度快
- 缺点:数组中进行插入、删除等操作比较繁琐耗时
-
链表:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。主要有以下优缺点:
- 优点:插入或者删除某个节点的元素很简单方便
- 缺点:查找某个位置节点的元素时需要挨个访问,比较耗时
-
哈希表:是根据关键码值而直接进行访问的数据结构。主要有以下优缺点:
- 优点:1、访问某个元素速度很快 2、插入删除操作也很方便
- 缺点:需要经过一系列运算比较复杂
方法缓存主要代码:
- 通过
sel和mask调用cache_hash进行计算出哈希表下标; - 通过
do-while循环防止哈希冲突; - 下标占用就调用cache_next再次哈希计算下标;
- 有问题的缓存则调用
bad_cache异常。
我们来看看这几个方法:
cache_hash算法
通过传入的sel和mask进行计算,将sel强转为uintptr_t类型,然后将得到的value值与上mask掩码
cache_next算法
非真机情况下通过上次计算出的下标加上1再次与上掩码mask;真机情况下如果i有值,则减1得到新下标,没有则直接取mask
incrementOccupied方法
_occupied自增
bad_cache方法
得到有问题的缓存,直接抛异常
总结
- 方法缓存是
bucket容器存储方法的sel和imp; - 方法缓存时每缓存一个方法
_occupied就会加1,当我们的_occupied+2大于容量capacity时系统就会进行扩容,也就是capacity*2,这也就解释了我们问题1数值变化问题;由于扩容会使得重新分配容量也就是调用了reallocate方法,所以我们问题3所出现的丢失就是因为扩容导致释放了老容器,回收内存; bucket通过哈希表实现存储的,每个bucket都是通过hash算法获取的下标,这也就能解释我们问题2为什么是无序的;
还有待探索,后续继续补充!