前言
在 類的底層原理(一) 和 類的底層原理(二) 中躬拢,分析了關于類的底層結構,包含 isa
食棕、superclass
晶姊、cache
尽狠、bits
。其中 bits
包含類的屬性,方法巍佑,代理斋配,成員變量等孔飒,以及類方法的獲取。
下面繼續(xù)探索類的結構艰争,關于 cache
坏瞄,其底層原理是什么?存在 cache
的意義又是什么甩卓?
準備工作
關于架構:
真機:
arm64
模擬器:
i386
mac:
__86_64__
__LP64__
:Unix 和 Unix類的系統(tǒng)
cache_t 結構
在分析 bits
內存偏移量時鸠匀,分析了關于 cache_t
占用內存字節(jié)數。
根據 cache_t
結構逾柿,雖然可以看到整體的數據結構缀棍,但是確定不了緩存數據保存位置宅此。是_bucketsAndMaybeMask
?還是 _originalPreoptCache
爬范?還有 sel
和 imp
在哪呢诽凌?目前并不知道,但是既然涉及到緩存坦敌,必然有增刪改查操作侣诵。
在
cache_t
中查找相關的方法:
插入方法:
所以:在
cache_t
中重點是bucket_t
。
bucket_t
bucket
是抽象意義的桶子狱窘,里面裝了若干的sel-imp
的映射對杜顺。
那么整個類關于cache的結構如下:
LLDB 驗證SEL和IMP
獲取 bucket_t
cache
的內存偏移量是16
,即0x10
但是直接通過 _bucketsAndMaybeMask
是拿不到數據的蘸炸。同樣的 _originalPreoptCache
的 Value
也獲取不到华望。
再次分析源碼找方法,有個 buckets()
方法
于是再次驗證
但是還是沒有征绎,發(fā)現 sel
拿不到:
這一步的結果其實在第一次獲取
cache
時已經證實了获洲,其中_maybeMask
和_occupied
都是0
,代表沒有方法淹禾。稍后解釋這兩個字段的實際意義馁菜。
調用實例方法,形成緩存
從 LLDB
打印結果來看铃岔,在調用實例方法之后汪疮,cache
里面有值了。
再次打印之后毁习,發(fā)現還是沒有獲取到 sel
智嚷,進行平移之后,index
為 6
時有數據了纺且。
獲取sel和imp
繼續(xù)分析下 bucket_t
的方法并找到了 sel()
和 imp()
方法
LLDB 獲取 sel
和 imp
這樣就能獲取 sel
和 imp
的值了盏道。
疑問:
為什么在
6
的位置?為什么
_maybeMask
值為7
载碌?
cache_t 模擬代碼分析
代碼模擬的好處:
方便我們進行代碼驗證猜嘱,而不是每次都是使用
LLDB
,因為LLDB
一旦出錯可能出現野指針的情況恐仑,需要重新驗證泉坐。遇到源碼無法調試的情況,可以進行調試裳仆。
小規(guī)模取樣的方式腕让,能對源碼的實現邏輯更清晰。
將 class
以及 cache
代碼模擬分析:
zl_objc_class
對應源碼objc_class
結構,因為objc_class
繼承objc_object
纯丸,所以有隱藏屬性ISA偏形。
zl_class_data_bits_t
對應源碼class_data_bits_t
結構,其中friend
修飾類不需要觉鼻,只有bits
屬性俊扭。
zl_cache_t
對應源碼cache_t
結構,其中_bucketsAndMaybeMask
保留坠陈,聯合體互斥原則萨惑,只需要包含_maybeMask
,_flags
仇矾,_occupied
的結構體庸蔼,結構體也可以簡化成三個屬性。
因為最終存儲的數據是 bucket_t
贮匕,所以還需要模擬下 bucket_t
的實現姐仅,由于之前論證 sel
和 imp
是通過 buckets()
獲取的,所以具體看一下 buckets()
方法實現:
通過方法分析:
_bucketsAndMaybeMask
通過load
獲取地址刻盐,再通過bucketsMask
掩碼獲取bucket_t *
數據掏膏。其實就是_bucketsAndMaybeMask
指向bucket_t *
數據。
zl_cache_t
簡化結構如下:
代碼驗證
打印結果:
_occupied
為1
敦锌,_maybeMask
為3
多個方法驗證
添加實例方法如下:
添加2個方法:
打印結果:
_occupied
為2
馒疹,_maybeMask
為3
添加3個方法:
打印結果:
_occupied
為1
,_maybeMask
為7
添加7個方法:
打印結果:
_occupied
為5
供屉,_maybeMask
為7
結論:
_occupied
為所占用個數行冰,_maybeMask
總容量大小。
類方法
不在類的cache
中伶丐,應該是在元類的cache
中。
_maybeMask
的值變化是因為擴容疯特,當發(fā)生擴容時哗魂,_occupied
會重新計數。之前的緩存也都被清空漓雅。
cache底層機制
想要了解緩存機制录别,必然要找關于插入的方法,從源碼分析邻吞,可以找到 insert()
函數组题。
insert()
首次
newOccupied
為1
,同時執(zhí)行isConstantEmptyCache
判斷抱冷,capacity
為4
崔列,創(chuàng)建容器時,由于oldCapacity
為0
,所以不需要釋放(freeOld
為false
)關于擴容條件:
__arm__ || __x86_64__ || __i386__
或者__arm64__ && !__LP64__
時:當容量大于等于3/4
擴容赵讯。
__arm64__ && __LP64__
時:當容量大于等于7/8
擴容盈咳。且當容量小于等于8
時允許占用100%
容量。拓展:
cache_fill_ratio
存在的意義其實是關于哈希函數中的負載因子
边翼,在3/4
和7/8
空間利用率最高鱼响。擴容數量:如果容量不為
0
,則為當前容量 * 2
组底,如果為0
丈积,則為4
。最大值MAX_CACHE_SIZE = 65536
债鸡。在擴容時直接釋放
了舊的緩存桶癣。
mask = capacity - 1
,這就是為什么第一次是3(4-1)娘锁,第二次擴容之后是7(4*2-1)的原因牙寞。占了一位存儲的是end_bucket_t
,格式為(sel-imp)0x1-buckets 指針地址)
cache_hash
計算插入起點hash
地址莫秆,之后插入時會通過cache_next
避免hash
碰撞沖突间雀。循環(huán)判斷通過set
函數插入bucket
數據。
reallocate
allocateBuckets
通過newCapacity
獲取新的bucket
setBucketsAndMask
存儲新的bucket
和mask
釋放舊的緩存
allocateBuckets
calloc
開辟內存镊屎。創(chuàng)建最后一個元素
endBucket
存儲為SEL-IMP(0x1-bucket address)
setBucketsAndMask
CACHE_MASK_STORAGE_OUTLINED
:是指__arm__ || __x86_64__ || i386
環(huán)境惹挟,只有newBuckets
存儲在_bucketsAndMaybeMask
中,意味著進行了強轉缝驳,_bucketsAndMaybeMask
中只有buckets
沒有mask
连锯。_maybeMask
沒有進行改變,直接使用capacity-1
用狱。
CACHE_MASK_STORAGE_HIGH_16 || CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
:是指OSX || SIMULATOR || 64位真機
機型运怖,buckets
和mask
都存儲在_bucketsAndMaybeMask
中,其中mask << maskShift
夏伊,此時maskShift
為48
摇展。
CACHE_MASK_STORAGE_LOW_4
:是指低32位
機型,buckets
和mask
都存儲在_bucketsAndMaybeMask
中溺忧,objc::mask16ShiftBits(mask)
方法的作用是:計算在16
位以下有多少位是0
咏连,_bucketsAndMaybeMask
也是存的這個個數值。
_bucketsAndMaybeMask.store()
設置bucket
和mask
的最新值重置
_occupied
鲁森,這里的_occupied
不包括自身的地址占用數祟滴。關于 內存排序規(guī)則(
memory_order_relaxed
/memory_order_release
) ,請看詳解 C++11的6種內存序總結
cache_hash
CONFIG_USE_PREOPT_CACHES
:表示arm64環(huán)境
真機歌溉。
sel地址
向右平移7
垄懂,并和sel地址
異或。
cache_next
__arm__ || __x86_64__ || __i386__
環(huán)境下向后插入(+
),__arm64__
環(huán)境下向前插入(-
)
(i+1) & mask
:向后插入埠偿,進行下一個按位與操作透罢。
i ? i-1 : mask
:向前插入,直接使用冠蒋,沒有按位與操作羽圃,當i = 0
時,返回mask
抖剿,相當于移動到了倒數第二個(最后一個存儲的是自身地址)朽寞。
cache屬性詳解 - _bucketsAndMaybeMask 內存分布
buckets()
方法如下:
mask()
方法如下:
__arm__ || __x86_64__ || __i386__
:_bucketsAndMaybeMask
存儲的只有buckets
,mask
需要直接從_maybeMask
字段讀取斩郎。
64位 OSX || SIMULATOR
:(1<<48) - 1
脑融,低48位
存儲buckets
,mask
存儲在高16位 (maskAndBuckets >> maskShift)
缩宜。
64 位真機
:(1 << 44)-1
肘迎,低44位
存儲buckets
,mask
存儲在高16位 (maskAndBuckets >> maskShift)
锻煌。
32位
:~((1<<4) -1)
:高60位
存儲buckets
妓布,mask
存儲在低4位 (0xffff >> maskShift)
。
疑問: 其中在獲取 64 位真機
環(huán)境下宋梧,低44位
存儲 buckets
匣沼,高16位
存儲 mask
。其中少了4位捂龄,在宏定義 64 位真機
中多了一個 maskZeroBits
的字段释涛,如下:
原因是:這
4
位為附加位,且必須為零倦沧。為objc_msgSend
使用唇撬。objc_msgSend
會使用這些附加位單個指令標明是來自_maskAndBuckets
的值。后面再詳細探究刀脏。