iOS底層-cache_t原理分析

前言

類的底層原理(一)類的底層原理(二) 中,分析了關(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

  • 模擬器:i386

  • mac:__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?還有 selimp 在哪呢?目前并不知道,但是既然涉及到緩存,必然有增刪改查操作。

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ù)的。同樣的 _originalPreoptCacheValue 也獲取不到。

再次分析源碼找方法,有個(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)行平移之后,index6 時(shí)有數(shù)據(jù)了。

獲取sel和imp

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

LLDB 獲取 selimp

這樣就能獲取 selimp 的值了。

疑問:

  • 為什么在 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),由于之前論證 selimp 是通過 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é)果:

_occupied1,_maybeMask3

多個(gè)方法驗(yàn)證

添加實(shí)例方法如下:

添加2個(gè)方法:

打印結(jié)果:

_occupied2,_maybeMask3

添加3個(gè)方法:

打印結(jié)果:

_occupied1_maybeMask7

添加7個(gè)方法:

打印結(jié)果:

_occupied5,_maybeMask7

結(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()

  • 首次 newOccupied1,同時(shí)執(zhí)行 isConstantEmptyCache 判斷,capacity4,創(chuàng)建容器時(shí),由于 oldCapacity0,所以不需要釋放(freeOldfalse

  • 關(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/47/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ǔ)新的 bucketmask

  • 釋放舊的緩存

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ī)型,bucketsmask 都存儲(chǔ)在 _bucketsAndMaybeMask 中,其中 mask << maskShift,此時(shí)maskShift48。

  • CACHE_MASK_STORAGE_LOW_4:是指低 32位 機(jī)型,bucketsmask 都存儲(chǔ)在 _bucketsAndMaybeMask 中,objc::mask16ShiftBits(mask) 方法的作用是:計(jì)算在 16 位以下有多少位是 0_bucketsAndMaybeMask 也是存的這個(gè)個(gè)數(shù)值。

  • _bucketsAndMaybeMask.store() 設(shè)置 bucketmask 的最新值

  • 重置 _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ǔ) bucketsmask 存儲(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整體流程圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容