iOS中的Cache_t流程詳解

前言:

在最近學習過程中我們知道一個類的結構的定義,以及一個對象的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的層級分布

我們接下來看看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,也就是selimp,在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)建對象,獲取對象的類搀崭,將斷點斷住相應的位置


    斷點調(diào)試.png
  • 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里邊存儲的類的selimp,從我們打印的結果知道,此處的_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í)行第一個方法港庄。再次打印結果;


    第二個斷點調(diào)試.png
  • 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 打印結果是


    兩個方法的打印結果.png

我們能看到 _occupied = 2_mask = 3 以及相關的方法對應的實現(xiàn) 也就是 selimp;

  • 8 我們把第4步的ISA 注釋掉瓣履,打印的結果卻是


    注釋掉類的ISA打印結果.png

我們能看到 _occupied = 0_mask = 5380272 未知情況

  • 9 我們再次打印4個方法查看打印結果率翅、[p say1][p say2]袖迎、[p say3]冕臭、[p say4]
    4個方法答應的結果圖.png

我們能看到 _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


    mask 變化前后圖.png

四鼻由、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ù)努力;如果大神們有什么好的建議請不吝賜教胀溺。謝謝裂七。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惭笑,一起剝皮案震驚了整個濱河市遗锣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌员舵,老刑警劉巖扯躺,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捉兴,死亡現(xiàn)場離奇詭異,居然都是意外死亡录语,警方通過查閱死者的電腦和手機倍啥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澎埠,“玉大人虽缕,你說我怎么就攤上這事∑盐龋” “怎么了氮趋?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長江耀。 經(jīng)常有香客問我剩胁,道長,這世上最難降的妖魔是什么祥国? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任昵观,我火速辦了婚禮,結果婚禮上舌稀,老公的妹妹穿的比我還像新娘啊犬。我一直安慰自己,他們只是感情好壁查,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布觉至。 她就那樣靜靜地躺著,像睡著了一般睡腿。 火紅的嫁衣襯著肌膚如雪语御。 梳的紋絲不亂的頭發(fā)上峻贮,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音沃暗,去河邊找鬼月洛。 笑死,一個胖子當著我的面吹牛孽锥,可吹牛的內(nèi)容都是我干的嚼黔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼惜辑,長吁一口氣:“原來是場噩夢啊……” “哼唬涧!你這毒婦竟也來了?” 一聲冷哼從身側響起盛撑,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤碎节,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抵卫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狮荔,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年介粘,在試婚紗的時候發(fā)現(xiàn)自己被綠了殖氏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡姻采,死狀恐怖雅采,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情慨亲,我是刑警寧澤婚瓜,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站刑棵,受9級特大地震影響巴刻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铐望,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一冈涧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧正蛙,春花似錦、人聲如沸营曼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蒂阱。三九已至锻全,卻和暖如春狂塘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鳄厌。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工荞胡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人了嚎。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓泪漂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親歪泳。 傳聞我的和親對象是個殘疾皇子萝勤,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345