前言:
在最近學習過程中我們知道一個類的結構的定義,以及一個對象的alloc
的執(zhí)行流程轮纫。初探底層的源碼腔寡。經(jīng)過最新開源的objc781我們知道,類的結構中重要的成員有
-
Class ISA
-
Class superclass
-
cache_t cache
-
class_data_bits_t bits
類的定義代碼如下
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
class_rw_t *data() const {
return bits.data();
}
......... //還包括很多數(shù)據(jù)和方法等
在之前的博客中我們曾對 isa
掌唾、class_data_bits_t
已經(jīng)進行了一個自我學習和總結的過程放前,接下來我們就針對類
中很重要的cache_t
再次深入進行一個自我學習和總結。希望通過這樣的學習糯彬、幫助自己更深刻的理解類的緩存
和工作原理
凭语。
一、cache_t 的環(huán)境結構
一個類的結構cache_t
大致流程如下:截圖來自Cooci老師的課件
我們接下來看看cache_t
的底層定義
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskShift = 48;
static constexpr uintptr_t maskZeroBits = 4;
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskBits = 4;
static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
1在虛擬模擬器中的結構
當我們編譯我們代碼中的時候撩扒,相關的環(huán)境已經(jīng)就確定了似扔;所以我們能看到模擬器和macOS中的結構是
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
public:
static bucket_t *emptyBuckets();
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
unsigned capacity();
bool isConstantEmptyCache();
bool canBeFreed();
再次進入_buckets
能看到 在模擬器和macOS中的結構
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
2在真機調(diào)試中的結構
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
public:
static bucket_t *emptyBuckets();
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
unsigned capacity();
bool isConstantEmptyCache();
bool canBeFreed();
再次進入_buckets
能看到 在模擬器和macOS中的結構
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
這就是cache_t
在各個環(huán)境中的代碼配置結構,編譯器會自動根據(jù)環(huán)境進入到指定的代碼進行編譯和運行。非此環(huán)境下的代碼我們想進入去查看是進不去炒辉,這就是編譯器的智能體現(xiàn)豪墅。
二、cache_t的SEL本丟查看
我們都知道 對象調(diào)用方法
都是通過編譯器
進行方法查找
黔寇。而編譯器會經(jīng)常查找的方法進行緩存
但校,下次進行方法查找的時候進行先進入緩存中查找
,這樣會大大節(jié)省時間啡氢,從而達到快速的作用状囱,cache_t
就是為此而生的。正好解決這個查找問題倘是。
接下來我們分兩種不同的環(huán)境進行調(diào)試和學習cache_t
的內(nèi)部_buckets
,也就是sel
和imp
,在iOS開發(fā)過程中亭枷,
- 1 源碼環(huán)境下指令查看
- 2 脫離源碼進行代碼答應
1,源碼環(huán)境下指令查看
首先我們創(chuàng)建一個類LGPerson
集成自NSObject
如下
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
在接下來進行相關的指令調(diào)試步驟查看相應的cache_t
-
1 創(chuàng)建對象,獲取對象的類搀崭,將斷點斷住相應的位置
2 在控制臺進行打印類信息
p/x pClass
結果是:
(Class) $0 = 0x00000001000022a8 LGPerson
3 進行偏移 我們知道
cache_t
和類地址相差16位叨粘,正好是0x10
所以cache_t是0x00000001000022b8
4 打印
cache_t
指針信息;
p (cache_t *)0x00000001000022b8
結果是
(cache_t *) $1 = 0x00000001000022b8
- 5 取出相關
cache_t
的內(nèi)容瘤睹;
p *$1
結果是
(cache_t) $2 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010032e430 {
_sel = {
std::__1::atomic<objc_selector *> = (null)
}
_imp = {
std::__1::atomic<unsigned long> = 0
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 0
}
_flags = 32804
_occupied = 0
}
- 6 我們知道類的層級結構后升敲,知道
_buckets
里邊存儲的類的sel
和imp
,從我們打印的結果知道,此處的_occupied = 0
轰传;也就是第一個斷點的位置還沒開始存儲sel
驴党,不信我們繼續(xù);
p $2.buckets()
結果是:
(bucket_t *) $3 = 0x000000010032e430
- 7 取出
buckets_t
中的內(nèi)容
p *$3
結果是
(bucket_t) $4 = {
_sel = {std::__1::atomic<objc_selector *> = (null}
_imp = {
std::__1::atomic<unsigned long> = 0
}
}
-
8 接下來我們過掉一個斷點获茬,執(zhí)行第一個方法港庄。再次打印結果;
9 再次打印
cache_t
中的內(nèi)容
p *$1
結果是
(cache_t) $5 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x0000000100661c50 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 10584
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 7
}
_flags = 32804
_occupied = 1
}
- 10 我們此時看到
_occupied = 1
也就是緩存中存在了我們調(diào)用的方法了:[p sayHello]
已經(jīng)完美執(zhí)行了恕曲,接下來我們再次驗證鹏氧;
p $5.buckets()
結構是
(bucket_t *) $6 = 0x0000000100661c50
- 11 取出
bucket_t
的內(nèi)容;
p *$6
結果是
(bucket_t) $7 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 10584
}
}
- 12 取出
sel
p $7.sel()
結果是
(SEL) $8 = "sayHello"
- 13 取出
imp
p $7.imp(pClass)
結果是
(IMP) $9 = 0x0000000100000bf0 (KCObjc`-[LGPerson sayHello])
同理過掉第二個斷點進入第三個佩谣,也可以進行相關的打印把还,_occupied = 2
用相關的指令
也能打印相關內(nèi)容;
2茸俭、脫離源碼進行代碼答應
從以文章開頭介紹吊履,cache_t
,依靠系統(tǒng)的幾部分內(nèi)容
- 1
_buckets
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
SEL _sel;
IMP _imp;
};
- 2
cache_t
struct lg_cache_t {
struct lg_bucket_t * _buckets;
mask_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
- 3
class_data_bits_t
struct lg_class_data_bits_t {
uintptr_t bits;
};
- 4
objc_class
struct lg_objc_class {
Class ISA;
Class superclass;
struct lg_cache_t cache; // formerly cache pointer and vtable
struct lg_class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
- 5 接下來創(chuàng)建類,并調(diào)用相關的兩個方法
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class]; // objc_clas
[p say1];
[p say2];
// [p say3];
// [p say4];
- 6 配置打印結果代碼
struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
NSLog(@"%hu - %u",lg_pClass->cache._occupied,lg_pClass->cache._mask);
for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
// 打印獲取的 bucket
struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
}
-
7 打印結果是
我們能看到 _occupied = 2
和 _mask = 3
以及相關的方法對應的實現(xiàn) 也就是 sel
和 imp
;
-
8 我們把第4步的ISA 注釋掉瓣履,打印的結果卻是
我們能看到 _occupied = 0
和 _mask = 5380272 未知情況
- 9 我們再次打印4個方法查看打印結果率翅、
[p say1]
、[p say2]
袖迎、[p say3]
冕臭、[p say4]
我們能看到 _occupied = 2
和 _mask = 7
,明確的知道m(xù)ask 已經(jīng)從原來的 3
變化到7
,那么為什么打印的方法還是只有兩個呢腺晾,這就是我們接下來研究的mask
的機制和擴容
的奧秘了。
三辜贵、cache_t 的buckets 和mask的機制探索
從上邊的問題 mask 已經(jīng)從原來的 3
變化到7
悯蝉,就是存在一個mask 的調(diào)整,那么mask 最大能到多少呢托慨?
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskShift = 48;
static constexpr uintptr_t maskZeroBits = 4;
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
- maskShift = 48
- maxMask = (1 << 16 ) - 1 = 2^16 -1
-
bucketsMask = (1<<44) - 1 = 2^44 -1
四鼻由、cache_t下的sel存儲機制
我們從objc781
開源代碼能清楚的知道cache_t 的過程是
- 1 cache_fill
- 2 cache_t::insert
- 3 cache_create
- 4 bcopy
- 5 flush_caches
- 6 cache_flush
- 7 cache_collect_free
1 cache_fill
我們知道創(chuàng)建一個方法需要先走cache_fill
,代碼定義如下:
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
if (cls->isInitialized()) {
cache_t *cache = getCache(cls);
cache->insert(cls, sel, imp, receiver);
}
}
2 cache_t::insert (最核心)
代碼定義如下
ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
// part1 計算相關的occupied
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// part2 判斷如果是創(chuàng)建 進行初始化
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
//part3 判斷是否需要擴容
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // 4 3 + 1 bucket cache_t
// Cache is less than 3/4 full. Use it as-is.
}
//part4 擴容操作;
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 擴容兩倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 內(nèi)存 庫容完畢
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//part5;進行相關的方法存儲
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(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));
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
首先將代碼的定義分配為5部分厚棵;代碼里已經(jīng)注釋的很清楚了
part1 計算相關的新的newOccupied
mask_t newOccupied = occupied() + 1;
part2.判讀第一次進行初始化操作
- 1 計算新值
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
- 2
INIT_CACHE_SIZE
的定義如下
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
也就是1 << 2,就是4.也就是說默認進來分配4的內(nèi)存空間蕉世;
- 3 再進行
setBucketsAndMask
setBucketsAndMask(newBuckets, newCapacity - 1);
具體函數(shù)就是
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
#ifdef __arm__
mega_barrier();
_buckets.store(newBuckets, memory_order::memory_order_relaxed);
mega_barrier();
_mask.store(newMask, memory_order::memory_order_relaxed);
_occupied = 0;
#elif __x86_64__ || i386
_buckets.store(newBuckets, memory_order::memory_order_release);
_mask.store(newMask, memory_order::memory_order_release);
_occupied = 0;
#else
也即是向內(nèi)存中存儲相關的sel
操作;再次把_occupied = 0
;也就是不占用任何空間婆硬,也就是初始化的的操作狠轻,只是一個空殼子,不存在實質(zhì)性的操作彬犯;
- 4 如果舊的值存在向楼,則全部釋放
cache_collect_free
static void cache_collect_free(bucket_t *data, mask_t capacity)
{
if (PrintCaches) recordDeadCache(capacity);
_garbage_make_room ();
garbage_byte_size += cache_t::bytesForCapacity(capacity);
garbage_refs[garbage_count++] = data;
cache_collect(false);
}
part3 如果新的值小于或等于原來的3/4,不做任何處理谐区;
if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // 4 3 + 1 bucket cache_t
// Cache is less than 3/4 full. Use it as-is.
}
part4.超過原來的3/4湖蜕,進行內(nèi)存擴容;
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 擴容兩倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true); // 內(nèi)存 庫容完畢
也就是將原來的內(nèi)存擴容到當前的2倍
宋列;然后始終保持mask_t m = capacity - 1;
這也就是為什么之前我們打印的mask從3變化到7的原因昭抒;
因為我們原來的內(nèi)存大小是4,因為同時執(zhí)行了4個方法虚茶,存儲已經(jīng)超過了原來的3/4,所以擴容到
8
.而根據(jù)mask_t m = capacity - 1;
戈鲁,所以原來的是mask = 4- 1 = 3
, 而新的mask = 8- 1 = 7
part5方法的存儲機制
- 1 在iOS開發(fā)中我們很多數(shù)據(jù)結構存儲都是以快速為主,例如
字典
,內(nèi)存映射
等嘹叫,其目的都是為了快速的查找想要的到的內(nèi)容。同理诈乒。cache_t
也不例外罩扇,其存儲的代碼如下
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
- 2 查看·
cache_hash
的內(nèi)部結構
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
3 通過上面的
cache_hash
和mask 進行相關的與
操作。我們都知道任何方法在內(nèi)存中都存在一個方法編號怕磨,用這個方法編號進行與操作喂饥,就能準確的得到這個方法在cache中的索引;4 如果得到的索引存在沖突肠鲫,則繼續(xù)處理hash 沖突员帮;
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(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));
通過這種方法,那么相關的方法在類cache_t
中就能有并且存儲是一個唯一的索引导饲,通過查找方法我們就能快速的查找到捞高;
五氯材、總結
通過將近5個小時的整理和斷點調(diào)試,終于寫完這次的內(nèi)容硝岗,雖然內(nèi)容過于簡單氢哮,但是還是自己實現(xiàn)了一遍流程,也算是一種收獲吧型檀,希望以后再接再厲冗尤。繼續(xù)努力;如果大神們有什么好的建議請不吝賜教胀溺。謝謝裂七。