前言
在 類的底層原理(一) 和 類的底層原理(二) 中,分析了關(guān)于類的底層結(jié)構(gòu),包含 isa、superclass、cache、bits。其中 bits 包含類的屬性,方法,代理,成員變量等,以及類方法的獲取。
下面繼續(xù)探索類的結(jié)構(gòu),關(guān)于 cache,其底層原理是什么?存在 cache 的意義又是什么?
準(zhǔn)備工作
關(guān)于架構(gòu):
真機(jī):
arm64模擬器:
i386mac:
__86_64__
__LP64__:Unix 和 Unix類的系統(tǒng)

cache_t 結(jié)構(gòu)
在分析 bits 內(nèi)存偏移量時(shí),分析了關(guān)于 cache_t 占用內(nèi)存字節(jié)數(shù)。

根據(jù) cache_t 結(jié)構(gòu),雖然可以看到整體的數(shù)據(jù)結(jié)構(gòu),但是確定不了緩存數(shù)據(jù)保存位置。是_bucketsAndMaybeMask?還是 _originalPreoptCache?還有 sel 和 imp 在哪呢?目前并不知道,但是既然涉及到緩存,必然有增刪改查操作。
在
cache_t中查找相關(guān)的方法:

插入方法:


所以:在
cache_t中重點(diǎn)是bucket_t。
bucket_t

bucket是抽象意義的桶子,里面裝了若干的sel-imp的映射對(duì)。
那么整個(gè)類關(guān)于cache的結(jié)構(gòu)如下:

LLDB 驗(yàn)證SEL和IMP
獲取 bucket_t
cache的內(nèi)存偏移量是16,即0x10

但是直接通過 _bucketsAndMaybeMask 是拿不到數(shù)據(jù)的。同樣的 _originalPreoptCache 的 Value 也獲取不到。

再次分析源碼找方法,有個(gè) buckets() 方法

于是再次驗(yàn)證

但是還是沒有,發(fā)現(xiàn) sel 拿不到:
這一步的結(jié)果其實(shí)在第一次獲取
cache時(shí)已經(jīng)證實(shí)了,其中_maybeMask和_occupied都是0,代表沒有方法。稍后解釋這兩個(gè)字段的實(shí)際意義。
調(diào)用實(shí)例方法,形成緩存

從 LLDB 打印結(jié)果來看,在調(diào)用實(shí)例方法之后,cache 里面有值了。

再次打印之后,發(fā)現(xiàn)還是沒有獲取到 sel,進(jìn)行平移之后,index 為 6 時(shí)有數(shù)據(jù)了。

獲取sel和imp
繼續(xù)分析下 bucket_t 的方法并找到了 sel() 和 imp() 方法

LLDB 獲取 sel 和 imp

這樣就能獲取 sel 和 imp 的值了。
疑問:
為什么在
6的位置?為什么
_maybeMask值為7?
cache_t 模擬代碼分析
代碼模擬的好處:
方便我們進(jìn)行代碼驗(yàn)證,而不是每次都是使用
LLDB,因?yàn)?LLDB一旦出錯(cuò)可能出現(xiàn)野指針的情況,需要重新驗(yàn)證。遇到源碼無法調(diào)試的情況,可以進(jìn)行調(diào)試。
小規(guī)模取樣的方式,能對(duì)源碼的實(shí)現(xiàn)邏輯更清晰。
將 class 以及 cache 代碼模擬分析:

zl_objc_class對(duì)應(yīng)源碼objc_class結(jié)構(gòu),因?yàn)?objc_class繼承objc_object,所以有隱藏屬性ISA。
zl_class_data_bits_t對(duì)應(yīng)源碼class_data_bits_t結(jié)構(gòu),其中friend修飾類不需要,只有bits屬性。
zl_cache_t對(duì)應(yīng)源碼cache_t結(jié)構(gòu),其中_bucketsAndMaybeMask保留,聯(lián)合體互斥原則,只需要包含_maybeMask,_flags,_occupied的結(jié)構(gòu)體,結(jié)構(gòu)體也可以簡(jiǎn)化成三個(gè)屬性。
因?yàn)樽罱K存儲(chǔ)的數(shù)據(jù)是 bucket_t ,所以還需要模擬下 bucket_t 的實(shí)現(xiàn),由于之前論證 sel 和 imp 是通過 buckets() 獲取的,所以具體看一下 buckets() 方法實(shí)現(xiàn):

通過方法分析:
_bucketsAndMaybeMask通過load獲取地址,再通過bucketsMask掩碼獲取bucket_t *數(shù)據(jù)。其實(shí)就是_bucketsAndMaybeMask指向bucket_t *數(shù)據(jù)。
zl_cache_t 簡(jiǎn)化結(jié)構(gòu)如下:

代碼驗(yàn)證

打印結(jié)果:

_occupied為1,_maybeMask為3
多個(gè)方法驗(yàn)證
添加實(shí)例方法如下:

添加2個(gè)方法:

打印結(jié)果:

_occupied為2,_maybeMask為3
添加3個(gè)方法:

打印結(jié)果:

_occupied為1,_maybeMask為7
添加7個(gè)方法:

打印結(jié)果:

_occupied為5,_maybeMask為7
結(jié)論:
_occupied為所占用個(gè)數(shù),_maybeMask總?cè)萘看笮 ?/p>
類方法不在類的cache中,應(yīng)該是在元類的cache中。
_maybeMask的值變化是因?yàn)閿U(kuò)容,當(dāng)發(fā)生擴(kuò)容時(shí),_occupied會(huì)重新計(jì)數(shù)。之前的緩存也都被清空。
cache底層機(jī)制
想要了解緩存機(jī)制,必然要找關(guān)于插入的方法,從源碼分析,可以找到 insert() 函數(shù)。
insert()

首次
newOccupied為1,同時(shí)執(zhí)行isConstantEmptyCache判斷,capacity為4,創(chuàng)建容器時(shí),由于oldCapacity為0,所以不需要釋放(freeOld為false)關(guān)于擴(kuò)容條件:
__arm__ || __x86_64__ || __i386__或者__arm64__ && !__LP64__時(shí):當(dāng)容量大于等于3/4擴(kuò)容。
__arm64__ && __LP64__時(shí):當(dāng)容量大于等于7/8擴(kuò)容。且當(dāng)容量小于等于8時(shí)允許占用100%容量。拓展:
cache_fill_ratio存在的意義其實(shí)是關(guān)于哈希函數(shù)中的負(fù)載因子,在3/4和7/8空間利用率最高。擴(kuò)容數(shù)量:如果容量不為
0,則為當(dāng)前容量 * 2,如果為0,則為4。最大值MAX_CACHE_SIZE = 65536。在擴(kuò)容時(shí)直接釋放了舊的緩存。
mask = capacity - 1,這就是為什么第一次是3(4-1),第二次擴(kuò)容之后是7(4*2-1)的原因。占了一位存儲(chǔ)的是end_bucket_t,格式為(sel-imp)0x1-buckets 指針地址)
cache_hash計(jì)算插入起點(diǎn)hash地址,之后插入時(shí)會(huì)通過cache_next避免hash碰撞沖突。循環(huán)判斷通過set函數(shù)插入bucket數(shù)據(jù)。
reallocate

allocateBuckets通過newCapacity獲取新的bucket
setBucketsAndMask存儲(chǔ)新的bucket和mask釋放舊的緩存
allocateBuckets

calloc開辟內(nèi)存。創(chuàng)建最后一個(gè)元素
endBucket存儲(chǔ)為SEL-IMP(0x1-bucket address)
setBucketsAndMask

CACHE_MASK_STORAGE_OUTLINED:是指__arm__ || __x86_64__ || i386環(huán)境,只有newBuckets存儲(chǔ)在_bucketsAndMaybeMask中,意味著進(jìn)行了強(qiáng)轉(zhuǎn),_bucketsAndMaybeMask中只有buckets沒有mask。_maybeMask沒有進(jìn)行改變,直接使用capacity-1。
CACHE_MASK_STORAGE_HIGH_16 || CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS:是指OSX || SIMULATOR || 64位真機(jī)機(jī)型,buckets和mask都存儲(chǔ)在_bucketsAndMaybeMask中,其中mask << maskShift,此時(shí)maskShift為48。
CACHE_MASK_STORAGE_LOW_4:是指低32位機(jī)型,buckets和mask都存儲(chǔ)在_bucketsAndMaybeMask中,objc::mask16ShiftBits(mask)方法的作用是:計(jì)算在16位以下有多少位是0,_bucketsAndMaybeMask也是存的這個(gè)個(gè)數(shù)值。
_bucketsAndMaybeMask.store()設(shè)置bucket和mask的最新值重置
_occupied,這里的_occupied不包括自身的地址占用數(shù)。關(guān)于 內(nèi)存排序規(guī)則(
memory_order_relaxed/memory_order_release) ,請(qǐng)看詳解 C++11的6種內(nèi)存序總結(jié)
cache_hash

CONFIG_USE_PREOPT_CACHES:表示arm64環(huán)境真機(jī)。
sel地址向右平移7,并和sel地址異或。
cache_next

__arm__ || __x86_64__ || __i386__環(huán)境下向后插入(+),__arm64__環(huán)境下向前插入(-)
(i+1) & mask:向后插入,進(jìn)行下一個(gè)按位與操作。
i ? i-1 : mask:向前插入,直接使用,沒有按位與操作,當(dāng)i = 0時(shí),返回mask,相當(dāng)于移動(dòng)到了倒數(shù)第二個(gè)(最后一個(gè)存儲(chǔ)的是自身地址)。
cache屬性詳解 - _bucketsAndMaybeMask 內(nèi)存分布
buckets() 方法如下:

mask() 方法如下:

__arm__ || __x86_64__ || __i386__:_bucketsAndMaybeMask存儲(chǔ)的只有buckets,mask需要直接從_maybeMask字段讀取。
64位 OSX || SIMULATOR:(1<<48) - 1,低48位存儲(chǔ)buckets,mask存儲(chǔ)在高16位 (maskAndBuckets >> maskShift)。
64 位真機(jī):(1 << 44)-1,低44位存儲(chǔ)buckets,mask存儲(chǔ)在高16位 (maskAndBuckets >> maskShift)。
32位:~((1<<4) -1):高60位存儲(chǔ)buckets,mask存儲(chǔ)在低4位 (0xffff >> maskShift)。
疑問: 其中在獲取 64 位真機(jī) 環(huán)境下,低44位 存儲(chǔ) buckets,高16位 存儲(chǔ) mask。其中少了4位,在宏定義 64 位真機(jī) 中多了一個(gè) maskZeroBits 的字段,如下:

原因是:這
4位為附加位,且必須為零。為objc_msgSend使用。objc_msgSend會(huì)使用這些附加位單個(gè)指令標(biāo)明是來自_maskAndBuckets的值。后面再詳細(xì)探究。

cache整體流程圖
