上篇文章探究了類的結(jié)構(gòu)纵诞,其中提到cache逼裆,今天就來探究一下。
-
結(jié)構(gòu)
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 父類
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
作用應該是調(diào)用方法后對其緩存,加快之后的調(diào)用速度。我們可以寫段代碼尚猿,檢驗一下:
Person *person = [Person alloc];
Class pClass = [Person class];
[person sayHello];
接著斷點運行打印內(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)
}
- 一開始
cache
沒有值,調(diào)用[person sayHello]
后才有了值楣富,由此可見凿掂,類的cache
緩存了調(diào)用過的實例方法。從而也可以推導出元類的cache
緩存了調(diào)用過的類方法:
(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ù)驗證:
Person *person = [[Person alloc] init];
Class pClass = [Person class];
[person sayHello];
[person sayCode];
[person sayNB];
接著斷點運行打印內(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
}
測試調(diào)用多個方法時菩彬,我們發(fā)現(xiàn)緩存的方法只有[Person sayNB]
缠劝,而且_mask
從3變成了7潮梯,這些看來是緩存策略影響了骗灶。
-
源碼
- 我們從objc4-750源碼探究,直入主題秉馏,從
cache_fill_nolock
函數(shù)開始耙旦。如果已緩存,就獲取返回:
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;//默認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();//超過3/4就擴容
}
// 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]
),還沒緩存帆竹,就調(diào)用reallocate
绕娘,這時capacity
為0,INIT_CACHE_SIZE
為4栽连,最終_mask
會等于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; //默認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 是一種算法,為了提前擴容秒紧,更安全
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);//清空舊的緩存, 所以擴容緩存后绢陌,舊的緩存沒了
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)過調(diào)多個方法后(最后調(diào)用
[person sayNB]
),_mask
經(jīng)過mask()+1
和newCapacity - 1
熔恢,此時應為4脐湾,緩存空間超過容量的3/4
(4 > 4*3/4),需要進行擴容:
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);//重置緩存大小
}
此時經(jīng)過reallocate
重置緩存大小并清空舊的緩存叙淌,所以只保留了[person sayNB]
秤掌,而且_mask
變成2倍
為8,再通過newCapacity - 1
變成7鹰霍。
- 緩存容量調(diào)整好后闻鉴,方法最終都會通過
bucket->set(key, imp)
緩存下來,方法數(shù)量也會通過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í)行效率衅谷;
- 類的
cache
用來緩存實例方法椒拗;- 元類的
cache
用來緩存類方法;- 如果已有緩存就獲取返回;如果沒有緩存就會創(chuàng)建容器緩存蚀苛;如果緩存超出容量的
3/4
就會擴容在验,變成2倍
,并且清空舊的緩存堵未;