上篇文章探究了類(lèi)的結(jié)構(gòu),其中提到cache,今天就來(lái)探究一下。
-
結(jié)構(gòu)
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 父類(lèi)
cache_t cache; // 緩存 // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
};
struct cache_t {
struct bucket_t *_buckets;//緩存方法
mask_t _mask;//緩存容量
mask_t _occupied;//緩存?zhèn)€數(shù)
...
};
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__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
...
};
-
作用
- 從結(jié)構(gòu)可以看出
cache作用應(yīng)該是調(diào)用方法后對(duì)其緩存,加快之后的調(diào)用速度。我們可以寫(xiě)段代碼,檢驗(yàn)一下:
Person *person = [Person alloc];
Class pClass = [Person class];
[person sayHello];
接著斷點(diǎn)運(yùn)行打印內(nèi)存結(jié)構(gòu):
2019-12-25 00:39:22.566292+0800 Test[3586:42169] Person say : -[Person sayHello]
(lldb) x/4gx pClass
0x1000012e0: 0x001d8001000012b9 0x0000000100b36140
0x1000012f0: 0x0000000101e23c20 0x0000000100000003
(lldb) p (cache_t *)0x1000012f0
(cache_t *) $1 = 0x00000001000012f0
(lldb) p *$1
(cache_t) $2 = {
_buckets = 0x0000000101e23c20
_mask = 3
_occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101e23c20
(lldb) p *$3
(bucket_t) $4 = {
_key = 4294971020
_imp = 0x0000000100000c60 (Test`-[Person sayHello] at Person.m:13)
}
- 一開(kāi)始
cache沒(méi)有值,調(diào)用[person sayHello]后才有了值,由此可見(jiàn),類(lèi)的cache緩存了調(diào)用過(guò)的實(shí)例方法。從而也可以推導(dǎo)出元類(lèi)的cache緩存了調(diào)用過(guò)的類(lèi)方法:
(lldb) p/x 0x001d8001000012b9 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00000001000012b8
(lldb) x/4gx 0x00000001000012b8
0x1000012b8: 0x001d800100b360f1 0x0000000100b360f0
0x1000012c8: 0x0000000101e236c0 0x0000000200000003
(lldb) p (cache_t *)0x1000012c8
(cache_t *) $6 = 0x00000001000012c8
(lldb) p *$6
(cache_t) $7 = {
_buckets = 0x0000000101e236c0
_mask = 3
_occupied = 2
}
(lldb) p $7._buckets
(bucket_t *) $8 = 0x0000000101e236c0
(lldb) p *$8
(bucket_t) $9 = {
_key = 4298994200
_imp = 0x00000001003cc3b0 (libobjc.A.dylib`::+[NSObject alloc]() at NSObject.mm:2294)
}
- 我們繼續(xù)驗(yàn)證:
Person *person = [[Person alloc] init];
Class pClass = [Person class];
[person sayHello];
[person sayCode];
[person sayNB];
接著斷點(diǎn)運(yùn)行打印內(nèi)存結(jié)構(gòu):
(lldb) x/4gx pClass
0x1000012e8: 0x001d8001000012c1 0x0000000100b36140
0x1000012f8: 0x0000000101029950 0x0000000100000007
(lldb) p (cache_t *)0x1000012f8
(cache_t *) $1 = 0x00000001000012f8
(lldb) p *$1
(cache_t) $2 = {
_buckets = 0x0000000101029950
_mask = 7
_occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101029950
(lldb) p *$3
(bucket_t) $4 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[0]
(bucket_t) $5 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[1]
(bucket_t) $6 = {
_key = 0
_imp = 0x0000000000000000
}
(lldb) p $2._buckets[2]
(bucket_t) $7 = {
_key = 4294971026
_imp = 0x0000000100000ce0 (Test`-[Person sayNB] at Person.m:25)
}
(lldb) p $2._buckets[3]
(bucket_t) $8 = {
_key = 0
_imp = 0x0000000000000000
}
測(cè)試調(diào)用多個(gè)方法時(shí),我們發(fā)現(xiàn)緩存的方法只有[Person sayNB],而且_mask從3變成了7,這些看來(lái)是緩存策略影響了。
-
源碼
- 我們從objc4-750源碼探究,直入主題,從
cache_fill_nolock函數(shù)開(kāi)始。如果已緩存,就獲取返回:
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;//如果已緩存,就獲取返回
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;//默認(rèn)0,遞增
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {//判斷緩存容器是否為空
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);//為空就創(chuàng)建
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();//超過(guò)3/4就擴(kuò)容
}
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);//緩存方法
}
- 第一次調(diào)用方法(
[person init]),還沒(méi)緩存,就調(diào)用reallocate,這時(shí)capacity為0,INIT_CACHE_SIZE為4,最終_mask會(huì)等于3:
struct bucket_t *cache_t::buckets()
{
return _buckets;
}
mask_t cache_t::mask()
{
return _mask;
}
mask_t cache_t::occupied()
{
return _occupied;
}
...
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0; //默認(rèn)0,有值+1
}
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2) //等于4
};
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);//創(chuàng)建
...
setBucketsAndMask(newBuckets, newCapacity - 1);// -1 是一種算法,為了提前擴(kuò)容,更安全
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);//清空舊的緩存, 所以擴(kuò)容緩存后,舊的緩存沒(méi)了
cache_collect(false);
}
}
bucket_t *allocateBuckets(mask_t newCapacity)
{
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);//初始化
...
return newBuckets;
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
...
_buckets = newBuckets;//新的容器
...
_mask = newMask;
_occupied = 0;//歸0
}
- 經(jīng)過(guò)調(diào)多個(gè)方法后(最后調(diào)用
[person sayNB]),_mask經(jīng)過(guò)mask()+1和newCapacity - 1,此時(shí)應(yīng)為4,緩存空間超過(guò)容量的3/4(4 > 4*3/4),需要進(jìn)行擴(kuò)容:
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;//變成2倍
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);//重置緩存大小
}
此時(shí)經(jīng)過(guò)reallocate重置緩存大小并清空舊的緩存,所以只保留了[person sayNB],而且_mask變成2倍為8,再通過(guò)newCapacity - 1變成7。
- 緩存容量調(diào)整好后,方法最終都會(huì)通過(guò)
bucket->set(key, imp)緩存下來(lái),方法數(shù)量也會(huì)通過(guò)incrementOccupied記錄:
void cache_t::incrementOccupied()
{
_occupied++;
}
void bucket_t::set(cache_key_t newKey, IMP newImp)
{
assert(_key == 0 || _key == newKey);
_imp = newImp;
if (_key != newKey) {
mega_barrier();
_key = newKey;
}
}
-
總結(jié)
- 方法緩存是為了提高程序的執(zhí)行效率;
- 類(lèi)的
cache用來(lái)緩存實(shí)例方法;- 元類(lèi)的
cache用來(lái)緩存類(lèi)方法;- 如果已有緩存就獲取返回;如果沒(méi)有緩存就會(huì)創(chuàng)建容器緩存;如果緩存超出容量的
3/4就會(huì)擴(kuò)容,變成2倍,并且清空舊的緩存;