我们先来回顾一下objc_class的几个主要的结构,如图:
image.png
主要是有4个变量:ISA
、superclass
、cache
、bits
,其中isa
和bits
已经分两篇介绍过了,superclass
这个比较简单我们就不介绍了,这篇就介绍一下cache
。
1、cache内存结构
cache
的内存结构在之前讲cache
的大小的时候已经介绍过一次,这里再展示一下:
1、_bucketsAndMaybeMask
是uintptr_t
类型
2、联合体:结构体里面有3个成员变量:mask_t
类型的_maybeMask
,_flags
和_occupied
都是uint16_t
类型的,他们的作用在后面会介绍
结构体cache_t
提供了一个获取buckets
的方法,返回的是结构体bucket_t
的指针:
再来看看bucket_t
的源码:
成员变量是两个:_imp
和_sel
,有一个获取sel
的方法sel()
以及获取imp
的方法(图中未展示,读者可以参考源码)
2、获取cache
有了获取bits
的基础,获取cache
就简单多了,获取bits
时,我们是从类的起始地址平移32
位,从objc_class
的结构可以看到,获取cache
时只需平移16
位就可以了,如图:
我们调一个方法之后,再来获取一次看看有没有什么变化:
image.png这个_occupied
发生了变化,那么他是不是有我们调用的cacheTest
这个方法导致的呢?
调一下buckets()
方法取到里面的内容:
这样我们就取到了buckets
存的sel
,那么怎么获取imp
呢,上面已经提过bucket_t
提供了获取imp
的方法:
按着这个方式我们可以获取到cacheTest
的imp
:
3、cache_t底层原理
上面只是缓存了一个方法,如果条用多个方法会不会都缓存呢?我们先在MlqqObject添加一个方法如下:
image.png然后再main函数里面按顺序调用:
image.png我们每调用一个方法打印一下_occupied
的值,看看会不会一直增加:
调1个方法_occupied
的值为1
:
调2个方法_occupied
的值为2
:
调3个方法_occupied
的值又为1
:
调4个方法_occupied
的值又为2
:
调5个方法_occupied
的值为3
:
调6个方法_occupied
的值为4
:
调7个方法_occupied
的值为5
:
调8个方法_occupied
的值又为1
:
从上面结果可以看到并不是调一个方法,_occupied
就增加1
,为什么会这样呢?
3.1 cache大小
我们通过源码来分析一下,全局搜索_occupied
:
找到了改变其值的方法incrementOccupied()
,再找调incrementOccupied()
的地方:
来分析主要代码块,看看苹果是怎么处理的:
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
定义一个新的变量的newOccupied
存储当前即将暂存的数量,变量oldCapacity
,capacity
存当前可以存储容量。
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
当cache
为空的时候,会走到这个分支来,主要是初始化cache
,调方法reallocate
开辟空间,当capacity
为0
时,开辟默认为INIT_CACHE_SIZE
大小的空间,INIT_CACHE_SIZE
可以查到为4
,也就是说当大小为0
时,默认开辟容量为4
的空间。这里注意一下reallocate
开辟空间函数的第三个参数传的是false
,从注释上也可以看到是不会释放旧的值。
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.
}
<!--__arm__ || __x86_64__ || __i386__-->
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
这个if
分支从注释中可以看到当newOccupied小于总容量的3/4不处理,这里用的是<=
,是因为加了一个CACHE_END_MARKER
的宏定义,该宏定义在模拟器下为1
,真机为0
,可以参考源码。
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {//capacity <= 8
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
这个分支不会走,因为CACHE_ALLOW_FULL_UTILIZATION
这个宏定义,在客户端没有定义,可以参考源码看一下。
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
当容量大于总容量的3/4
时,就会扩容,扩容到之前的2
倍,最大值为MAX_CACHE_SIZE
,为1<<16=65536
,再调用reallocate
方法开辟空间时,第三个参数传的值就是true
了,这时就会清除旧值了。
看一下reallocate
这个容函数做了什么。
这里面要介绍一下setBucketsAndMask
这个方法,他有两个参数第一个是newBuckets
,第二个就是mask
:
存储的方法:
_bucketsAndMaybeMask.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, memory_order_relaxed);
((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets
的意思是将newmask
向左移动maskShift
位,maskShift
可以从cache_t
的定位中查到为48
,然后再或newBuckets
,结果就是将mask放在_bucketsAndMaybeMask
的高16
位,buckets
放在低48
位。
3.2 cache存储
处理完空间问题,再来看看怎么存的
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
获取到当前buckets
指针b
,根据capacit
计算出mask
值m
,根据mask
和sel
计算出脚标begin
。
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));
这里是一个do-while
循环,为什么要一个循环呢,因为上面采用cache_hash
计算出来的脚标不同的sel
又可能是相同的,所以要循环遍历一下。在循环体里面第一个if
判断b[i].sel() == 0
,说明该位置没有值,就会使_occupied+1
,并且把sel
保存在当前位置。第二个if
,如果当前位置有值并且是要缓存的sel
时就不处理了,直接return
了。
经过上面的介绍,我们根据我们看到的现象来分析一下是不是这样的,是mac下运行的故CACHE_END_MARKER
为1
:
- 当调
1
个方法时,第一次初始化cache
大小capacity
为4
,capacity * 3/4 = 3
, 这时newOccupied
为1
,满足(1+CACHE_END_MARKER = 2) <= 3
,不会扩容; - 当调
2
个方法时,capacity * 3/4 = 3
,这时newOccupied
为2
,满足(newOccupied+CACHE_END_MARKER = 3) <= 3
,不会扩容; - 当调
3
个方法时,capacity * 3/4 = 3
,这时newOccupied
为3
,不满足(newOccupied+CACHE_END_MARKER = 4) <= 3
,会扩容,清除旧的值再保存第三个方法,_occupied
变为1
。 - 当调
4
个方法时,capacity
已经扩容为8
了,capacity * 3/4 = 6
,这时newOccupied
为2
,因为已经存了第3
个方法,满足(newOccupied+CACHE_END_MARKER = 3) <= 6
,不会扩容; - 当调
5
个方法时,capacity
为8
,capacity * 3/4 = 6
,这时newOccupied
为3
,满足(newOccupied+CACHE_END_MARKER = 4) <= 6
,不会扩容; - 当调
6
个方法时,capacity
为8
,capacity * 3/4 = 6
,这时newOccupied
为4
,满足(newOccupied+CACHE_END_MARKER = 5) <= 6
,不会扩容; - 当调
7
个方法时,capacity
为8
,capacity * 3/4 = 6
,这时newOccupied
为5
,满足(newOccupied+CACHE_END_MARKER = 6) <= 6
,不会扩容; - 当调
8
个方法时,capacity
为8
,capacity * 3/4 = 6
,这时newOccupied
为6
,不满足(newOccupied+CACHE_END_MARKER = 7) <= 6
,需要扩容;扩容结果capacity
变为16
,capacity * 3/4 = 12
,清除旧值再存第8
个方法,_occupied
变为1
。
到这里cache的底层原理就介绍完了,这里只是缓存,那么什么时候取呢,请看下一篇:消息流程之快速查找。
网友评论