objc_class底層cache_t詳解

cache_t 結構解析

類的底層原理探索 中我們了解了objc_class中存儲了isa,superClass,cache滓技,bits你踩,今天我們來看下cache的作用和底層實現(xiàn)。

cache結構

image.png

這個結構并不能看出來cache的作用,所以我們通過內(nèi)存偏移打印一下cache的內(nèi)容
image.png

我們打印的信息和其數(shù)據(jù)結構一致击你,但是緩存的內(nèi)容我們還是不知道剃氧,我們只能從源碼中繼續(xù)往下看敏储。

image.png

原碼中有一個insert方法,應該是用來存入數(shù)據(jù)的朋鞍。

進入這個insert函數(shù)來看一下


image.png

我們注意到insert方法的參數(shù)有SEL,IMP,receiver,并且將這些參數(shù)放在了bucket中已添,我們來驗證一下


獲取bucket_t的內(nèi)存內(nèi)容

bucket_t看到了sel和imp,但是輸出的內(nèi)容又看不懂了滥酥,來看看bucket_t的源碼聲明:

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__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
        return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

    // Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }

#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
    inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
        return (IMP)imp;
    }

    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

    inline void scribbleIMP(uintptr_t value) {
        _imp.store(value, memory_order_relaxed);
    }

    template <Atomicity, IMPEncoding>
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

我們看到 調用sel()函數(shù)可以返回SEL更舞,調用imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)可以返回IMP,于是就可以試試在內(nèi)存上獲取到的bucket_t去調用這兩個函數(shù):


調用sel()和imp()函數(shù)
cache_t

cache_t擴容

那方法能存多少呢坎吻?存滿了又是如何擴容呢缆蝉?buckets是如何擴容的?為什么我沒調用class和respondsToSelector方法它們就緩存到了buckets里面了瘦真?
看下cache_t的insert函數(shù)的實現(xiàn)代碼

void cache_t::insert(SEL sel, IMP imp, id receiver)
{   
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1; // 第一次insert的時候occupied()即_occupied會是0刊头,newOccupied會是1
    // capacity的值就是buckets的長度
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    // 如果cache為空,則分配 arm64下長度為2 x86_64下長度為4的buckets诸尽,reallocate里無需釋放老buckets
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        // 給容量附上初始值原杂,x86_64為4,arm64為2
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    // 在arm64下您机,緩存的大小 <= buckets長度的7/8  不擴容
    // 在x86_64下穿肄,緩存的大小 <= buckets長度的3/4  不擴容
    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 // 只有arm64才需要走這個判斷
    // 在arm64下,buckets的長度 < = 8 時际看,不擴容
    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 { // 除卻上面的邏輯被碗,就是擴容邏輯了
        // 對當前容量的2倍擴容,并且如果擴容后容量大小 大于 一個最大閾值仿村,則設置為這個最大值
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        // 創(chuàng)建新的擴容后的buckets,釋放舊的bukets
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets(); // 獲取buckets數(shù)組指針
    mask_t m = capacity - 1; // m是buckets的長度-1
    mask_t begin = cache_hash(sel, m);// 通過hash計算出要插入的方法在buckets上的起始位置(begin不會超過buckets的長度-1)
    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)) { // 當前hash計算出來的buckets在i的位置它有沒有值锐朴,如果沒有值就去存方法
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) { // 當前hash計算出來的buckets在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)); // 如果計算出來的起始位置i存在hash沖突的話焚志,就通過cache_next去改變i的值(增大i)

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

從代碼上看衣迷,第一次空了會進行初始化capacity的長度INIT_CACHE_SIZE.


image.png

image.png

在arm64架構下開辟一個長度為2的桶子,在x86_64架構下開辟長度為4的桶子

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.
    }

在arm64架構下如果緩存大于等于桶子長度的7/8酱酬,在x86_64架構下緩存大小大于等于桶子長度的3/4 則什么也不干壶谒。
在arm64架構下,當桶子的長度小于等于8的時候什么也不干
當緩存長度大于(系統(tǒng)設定的)默認最大值就等于默認最大值
其他情況下需要擴容膳沽,擴容的大小為2倍汗菜。

所以我們就明白了為什么之前調用了方法之后會什么沒有找到,arm64下挑社,初始值為2陨界,當?shù)?個方法緩存的時候,則要進行兩倍擴容為4痛阻,并且需要清除舊桶菌瘪。所以instanceMethod在剛進來擴容的時候就被清除掉了,也就找不到了阱当。而前面我們說到class方法和responseToSelector方法是我們在調試的時候通過lldb調用產(chǎn)生的俏扩。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市弊添,隨后出現(xiàn)的幾起案子录淡,更是在濱河造成了極大的恐慌,老刑警劉巖油坝,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赁咙,死亡現(xiàn)場離奇詭異,居然都是意外死亡免钻,警方通過查閱死者的電腦和手機彼水,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來极舔,“玉大人凤覆,你說我怎么就攤上這事〔鹞海” “怎么了盯桦?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渤刃。 經(jīng)常有香客問我拥峦,道長,這世上最難降的妖魔是什么卖子? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任略号,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘玄柠。我一直安慰自己突梦,他們只是感情好,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布羽利。 她就那樣靜靜地躺著宫患,像睡著了一般。 火紅的嫁衣襯著肌膚如雪这弧。 梳的紋絲不亂的頭發(fā)上娃闲,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音匾浪,去河邊找鬼皇帮。 笑死,一個胖子當著我的面吹牛户矢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播殉疼,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼梯浪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓢娜?” 一聲冷哼從身側響起挂洛,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎眠砾,沒想到半個月后虏劲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡褒颈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年柒巫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谷丸。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡堡掏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刨疼,到底是詐尸還是另有隱情泉唁,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布揩慕,位于F島的核電站亭畜,受9級特大地震影響,放射性物質發(fā)生泄漏迎卤。R本人自食惡果不足惜拴鸵,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宝踪,春花似錦侨糟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厉膀,卻和暖如春溶耘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背服鹅。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工凳兵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人企软。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓庐扫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仗哨。 傳聞我的和親對象是個殘疾皇子形庭,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容