在前面的文章中,我們探索了isa、superclass、bits屬性
iOS底層原理07:類 & 類結(jié)構(gòu)分析
iOS底層原理08:類結(jié)構(gòu)分析——bits屬性
本文主要探索cache的結(jié)構(gòu)和底層原理
1、探索cache的數(shù)據(jù)結(jié)構(gòu)
cache的類型是cache_t結(jié)構(gòu)體
1.1、cache_t結(jié)構(gòu)體
??來(lái)看看objc4-818源碼中cache_t結(jié)構(gòu)體
typedef unsigned long uintptr_t;
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
struct cache_t {
private:
// explicit_atomic 顯示原子性,目的是為了能夠 保證 增刪改查時(shí) 線程的安全性
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //8字節(jié)
union {
struct {
explicit_atomic<mask_t> _maybeMask; //4字節(jié)
#if __LP64__
uint16_t _flags; //2字節(jié)
#endif
uint16_t _occupied;//2字節(jié)
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字節(jié)
};
//下面是一些static屬性和方法,并不影響結(jié)構(gòu)體的內(nèi)存大小,主要是因?yàn)閟tatic類型的屬性 不存在結(jié)構(gòu)體的內(nèi)存中
/*
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
// macOS 或 __arm64__的模擬器
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
//__arm64__的真機(jī)
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
//32位 真機(jī)
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
//macOS 模擬器
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
****** 中間是不同的架構(gòu)之間的判斷 主要是用來(lái)不同類型 mask 和 buckets 的掩碼
*/
// ...省略代碼
// 下面是幾個(gè)比較重要的方法
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
void insert(SEL sel, IMP imp, id receiver);
/// 快速計(jì)算對(duì)象內(nèi)存大小,16字節(jié)對(duì)齊,在對(duì)象的alloc中我們已經(jīng)分析過(guò)了
size_t fastInstanceSize(size_t extra) const {...}
// ...省略代碼
}
cache_t是結(jié)構(gòu)體類型,有兩個(gè)成員變量:_bucketsAndMaybeMask和一個(gè)聯(lián)合體
-
_bucketsAndMaybeMask是uintptr_t類型,占8字節(jié) - 聯(lián)合體里面有兩個(gè)成員變量:
結(jié)構(gòu)體和_originalPreoptCache,聯(lián)合體由最大的成員變量的大小決定-
_originalPreoptCache是preopt_cache_t *結(jié)構(gòu)體指針,占8字節(jié) - 結(jié)構(gòu)體中有
_maybeMask、_flags、_occupied三個(gè)成員變量。-
_maybeMask的大小取決于mask_t即uint32_t,占4字節(jié) -
_flags是uint16_t類型,占2字節(jié) -
_occupied是uint16_t類型,占2字節(jié)
-
-
所以cache_t的大小等于 8+8或者8+4+2+2,即16字節(jié)
-
cache_t結(jié)構(gòu)體提供了buckets()方法,返回類型是bucket_t *結(jié)構(gòu)體指針
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
-
cache_t結(jié)構(gòu)體還提供了insert方法,插入sel和imp,即對(duì)方法的緩存
void cache_t::insert(SEL sel, IMP imp, id receiver) {
//對(duì)各種不符合條件的判斷報(bào)出錯(cuò)誤碼
//省略代碼。。。。
//通過(guò)buckets數(shù)組來(lái)判斷需要插入的內(nèi)容情況
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//省略代碼。。。。
}
1.2、bucket_t結(jié)構(gòu)體
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__ // arm64架構(gòu)
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else // 其他架構(gòu)
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
// ...省略代碼
}
-
bucket_t的成員順序與架構(gòu)有關(guān) -
bucket_t有兩個(gè)成員變量_sel和_imp,存儲(chǔ)方法的信息
1.3、lldb調(diào)試驗(yàn)證
- 創(chuàng)建
HTPerson類

-
main.m中代碼如下,在[p sayHello];設(shè)置斷點(diǎn),運(yùn)行代碼

- 通過(guò)
p *$1查看cache的值,此時(shí)_maybeMask.Value和_occupied的值都為0

- 執(zhí)行完
[p sayHello];對(duì)象方法,繼續(xù)查看cache的值,此時(shí)_maybeMask.Value和_occupied的值都發(fā)生了變化

- 完整的
lldb調(diào)試過(guò)程如下圖

通過(guò)源碼和lldb調(diào)試,可以發(fā)現(xiàn) cache存儲(chǔ)的 方法緩存
- 調(diào)用對(duì)象方法
sayHello后,_maybeMask和_occupied被賦值,這兩個(gè)變量應(yīng)該和緩存是有關(guān)系的,我們?cè)诤竺孢M(jìn)行深入分析。 -
bucket_t結(jié)構(gòu)體提供了sel()和imp(nil, Class)方法
2、根據(jù)源碼,對(duì)類和cache進(jìn)行仿寫
為什么需要進(jìn)行代碼仿寫呢?
- 當(dāng)
源碼無(wú)法直接運(yùn)行調(diào)試時(shí),就需要進(jìn)行代碼仿寫; - 使用
lldb調(diào)試時(shí),增減一些屬性、方法,就需要再次執(zhí)行比較多的重復(fù)步驟,比較繁瑣; - 小規(guī)模取樣的方式,會(huì)讓你對(duì)底層更加清晰。
2.1、準(zhǔn)備工作
- 新建一個(gè)
macOS -> Command Line Tool工程,并創(chuàng)建HTPerson類,代碼如下:
/*** HTPerson.h ***/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HTPerson : NSObject
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)sayHappy;
@end
NS_ASSUME_NONNULL_END
/*** HTPerson.m ***/
#import "HTPerson.h"
@implementation HTPerson
- (void)say1{
NSLog(@"%s",__func__);
}
- (void)say2{
NSLog(@"%s",__func__);
}
- (void)say3{
NSLog(@"%s",__func__);
}
- (void)say4{
NSLog(@"%s",__func__);
}
- (void)say5{
NSLog(@"%s",__func__);
}
- (void)say6{
NSLog(@"%s",__func__);
}
- (void)say7{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"%s",__func__);
}
@end
-
main.m文件中代碼如下:
#import <Foundation/Foundation.h>
#import "HTPerson.h"
typedef uint32_t mask_t;
// bucket_t結(jié)構(gòu)體
struct ht_bucket_t {
SEL _sel;
IMP _imp;
};
// cache_t結(jié)構(gòu)體
struct ht_cache_t {
struct ht_bucket_t * _buckets; //8字節(jié)
mask_t _maybeMask; //4字節(jié)
uint16_t _flags; //2字節(jié)
uint16_t _occupied;//2字節(jié)
};
// 類結(jié)構(gòu)體
struct ht_objc_class {
Class isa; // 8字節(jié)
Class superclass; //8字節(jié)
struct ht_cache_t cache; //16字節(jié)
uintptr_t bits; // 8字節(jié)
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson *p = [HTPerson alloc];
Class pClass = p.class;
// [p say1];
// [p say2];
// [p say3];
// [p say4];
// [p say5];
// [p say6];
// [p say1];
// [p say2];
struct ht_objc_class * ht_class = (__bridge struct ht_objc_class *)(pClass);
NSLog(@"- %hu - %u", ht_class->cache._occupied, ht_class->cache._maybeMask);
for (int i = 0; i < ht_class->cache._maybeMask; i++) {
struct ht_bucket_t bucket = ht_class->cache._buckets[I];
NSLog(@"%@ - %pf", NSStringFromSelector(bucket._sel), bucket._imp);
}
}
return 0;
}
2.2、對(duì)象方法的調(diào)用 與 cache值的關(guān)系
- 未調(diào)用對(duì)象方法
如果對(duì)象方法都沒(méi)有調(diào)用,則cache不會(huì)進(jìn)行方法緩存,此時(shí)_occupied和_maybeMask的值都為0

- 調(diào)用
say1和say2方法,查看打印結(jié)果

- 如果繼續(xù)調(diào)用
say3、say4和say5方法呢

【問(wèn)題】 這里就產(chǎn)生了幾個(gè)疑問(wèn)?
-
_occupied和_maybeMask是什么?他們的值是如何變化? - 調(diào)用
say3、say4和say5方法后,say1和say2怎么消失了? -
cache存儲(chǔ)的位置怎么是亂序的呢?
_occupied和_maybeMask是什么?在什么地方賦值,只能去objc源碼中找答案。我們要緩存方法,首先看怎么把方法插入到bukets中的。帶著這些疑問(wèn)繼續(xù)探討cache_t源碼
3、cache_t源碼探究
- 首先找到
方法緩存的入口

從這個(gè)插入的方法來(lái)看,插入的參數(shù)有sel、imp、還有receiver消息接收者。??下面是這個(gè)插入方法的代碼實(shí)現(xiàn):
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
// ...省略代碼 (錯(cuò)誤處理相關(guān)代碼)
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */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.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
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));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
計(jì)算當(dāng)前所占容量

-
occupied()獲取當(dāng)前所占的容量,其實(shí)就是告訴你緩存中已經(jīng)有幾個(gè)bucket -
newOccupied = occupied() + 1,表示當(dāng)前方法第幾個(gè)進(jìn)來(lái)緩存的 -
oldCapacity目的是為了重新擴(kuò)容的時(shí)候釋放舊的內(nèi)存
開(kāi)辟容量

- 第一次緩存方法的時(shí),開(kāi)辟默認(rèn)容量是
capacity = INIT_CACHE_SIZE即capacity = 4就是4個(gè)bucket的內(nèi)存大小 -
reallocate(oldCapacity, capacity, /* freeOld */false)開(kāi)辟內(nèi)存,freeOld變量控制是否釋放舊的內(nèi)存
reallocate方法探究

reallocate 方法主要做三件事
-
allocateBuckets開(kāi)辟內(nèi)存 -
setBucketsAndMask設(shè)置mask和buckets的值 -
collect_free是否釋放舊的內(nèi)存,由freeOld控制
allocateBuckets方法(開(kāi)辟內(nèi)存)探究

allocateBuckets 方法主要做兩件事
-
calloc(bytesForCapacity(newCapacity), 1)開(kāi)辟內(nèi)存 -
end->set將開(kāi)辟內(nèi)存的最后一個(gè)位置存入sel = 1,imp = 第一個(gè)buket位置的地址
setBucketsAndMask 方法探究

setBucketsAndMask方法主要用來(lái)賦值
- 根據(jù)不同的架構(gòu)系統(tǒng)向
_bucketsAndMaybeMask和_maybeMask寫入數(shù)據(jù) -
_occupied重置為0
collect_free 方法探究

-
collect_free方法主要是清空數(shù)據(jù),回收內(nèi)存
二倍擴(kuò)容

- 當(dāng)
方法緩存到總?cè)萘康?code>3/4或者7/8時(shí),回進(jìn)行二倍擴(kuò)容 -
二倍擴(kuò)容即開(kāi)辟2倍新內(nèi)存,釋放舊內(nèi)存
方法緩存

- 首先拿到
buckets(),即開(kāi)辟這塊內(nèi)存首地址,也就是第一個(gè)bucket的地址,buckets()既不是數(shù)組也不是鏈表,只是一塊連續(xù)的內(nèi)存 -
cache_hash方法計(jì)算hash下標(biāo),cache_next方法處理hash沖突 - 如果當(dāng)前的位置沒(méi)有數(shù)據(jù),就緩存該方法;如果該位置有方法且和你的方法一樣的,說(shuō)明該方法緩存過(guò)了,直接
return;如果存在hash沖突,下標(biāo)一樣,sel不一樣,此時(shí)會(huì)進(jìn)行再次hash,沖突解決繼續(xù)緩存
方法緩存寫入方法 set

set方法:將imp和sel寫入bucket中
insert方法調(diào)用流程
前面探究了insert方法的源碼實(shí)現(xiàn),接下來(lái)我們探究insert方法調(diào)用流程,是如何從調(diào)用實(shí)例方法走到cache里面的insert方法的?
- 首先在
insert方法中打個(gè)斷點(diǎn),然后運(yùn)行源碼,查看函數(shù)調(diào)用棧

從堆棧信息可以看出insert的調(diào)用流程:_objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert
【問(wèn)題】 _objc_msgSend_uncached方法又是何時(shí)調(diào)用的呢?
- 在
objc4-818源碼中搜索_objc_msgSend_uncached如下圖

我們發(fā)現(xiàn):objc_msgSend方法會(huì)調(diào)用_objc_msgSend_uncached,至此整個(gè)流程就串聯(lián)起來(lái)了
- 方法調(diào)用的本質(zhì)就是
消息發(fā)送,即調(diào)用objc_msgSend - 方法緩存的調(diào)用流程:
objc_msgSend-->_objc_msgSend_uncached-->lookUpImpOrForward-->log_and_fill_cache-->cache_t::insert