cache原理分析

前言:

在類的結構分析這篇分析了objc_class 結構體內部的isa和bit屬性,那麼這次就分析其中的cache屬性。

cache分析

部分源碼:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    //macOS、模擬器
    //explicit_atomic 原子性,目的是為了能夠保證增刪改查線程的安全性。
    //等價於struct bucket_t * _buckets;
    //_buckets 中放的是 sel imp
    //_buckets的讀取有提供相應名稱的方法buckets()
    explicit_atomic _buckets;
    explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //64位真機
    explicit_atomic _maskAndBuckets;//寫在一起的目的是為了優化
    mask_t _mask_unused;
    //以下都是掩码,即面具 -- 類似于isa的掩碼,即位域
    // 掩碼省略....

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真机
    explicit_atomic _maskAndBuckets;
    mask_t _mask_unused;

    //以下都是掩码,即面具 -- 类似于isa的掩码,即位域
    // 掩码省略....
#else
#error Unknown cache mask storage type.
#endif

#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
    //方法省略.....
}

  • 以上是cache_t 源碼,可以看到三個架構處理,其中真機架構中的mask和bucket是寫在一起,目的是為了優化,透過不同的架構提供的掩碼來獲取相應的數據。

    • CACHE_MASK_STORAGE_OUTLINED:運行在macOS或是模擬器
    • CACHE_MASK_STORAGE_HIGH_16 :運行在64位的真機
    • CACHE_MASK_STORAGE_LOW_4 :運行在非64位的真機
  • 可以看到cache_t 源碼內部有bucket_t 結構體,內部存放selimp

    struct bucket_t {
    private:
    #if __arm64__ //真機
        //explicit_atomic 是加了原子性的保护
        explicit_atomic _imp;
        explicit_atomic _sel;
    #else //非真機
        explicit_atomic _sel;
        explicit_atomic _imp;
    #endif
        //方法等其他部分省略
    }
    
    

查找cache中的sel-imp

cache_t 中查找存儲的sel-imp,有以下兩種方式

  • 通過源碼
  • 脫離源碼環境環境調適分析

事前準備

  • 定義一個LGPeron類的聲明
  • LGPeron類的實現
  • 在main中定義LGPerson類型指針p,並調用其中其中的三個實例方法,在p調用第一個方法第一個方法處加一個斷點。
  • ldb查找流程如下
  • 透過獲取pClass的首地址,偏移16字節後,得到了cache地址
  • 從源碼分析中,我們知道sel-impcache_t_buckets屬性 中,而在cache_t結構體中提供了獲取_buckets屬性的方法buckets()
  • 獲取了_buckets屬性,就可以獲取sel-imp了,這兩個的獲取在bucket_t結構體中同樣提供了相應的獲取方法sel()以及imp(pClass)
  • 可以從上面的流程看到,在未執行方法前,緩存是沒有沒有緩存方法的,而在調用方法後,緩存內就有一個緩存。
  • 我們現在了解如何獲取sel-imp,我們可以透過machOView來進行確認,將二進制文件拖入machOView 可以在Function段內查看到地址與我們lldb打印出來的地址一樣
  • 透過上面的成功獲取了sel-imp,那麼我們接著再打一個斷點在sayCode上,過掉斷點後,執行了sayCode方法,用上面的步驟,先打印cache,可以看到_occupied = 2 ,接著打印buckets以及透過buckets的首地址偏移拿到第二個sel-imp,然後打印第二個sel 及 imp,即可看到結果。

脫離源碼調適分析

  • 上述可透過源碼的環境進行lldb動態調適,那麼我們來試試如果不使用源碼環境的狀況下,要如何分析sel-imp呢?
  • 其實也就是將所需部分的源碼,拷貝至文件中,完整程式碼如下
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct lg_cache_t {
    struct lg_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct lg_class_data_bits_t {
    uintptr_t bits;
};

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];  // objc_clas
        [p say1];
        [p say2];
        //[p say3];
        //[p say4];

        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; icache._mask; i++) {
            // 打印获取的 bucket
            struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }

        NSLog(@"Hello, World!");
    }
    return 0;
}

  • 由於objc_classISA屬性是繼承自objc_object的,我們將源碼拷貝到自己的文件時,就缺少了繼承關係,所以需要聲明這個成員變量,即添加 Class ISAstruct lg_objc_class中,否則會有下圖之狀況。
  • 在lg_objc_class增加Class ISA屬性。
  • 增加兩個方法say3 say4。

我們可以從上面的打印結果,發現以下的問題?

  • 什麼是_mask_occupied
  • 為什麼通過方法調用的增加,打印的_occupied_mask 會變化
  • bucket 數據為什麼丟失情況會有?
  • 為什麼是say4先打印,say3後打印,明顯順序有問題?為什麼是say4先打印,say3後打印,明顯順序有問題?
  • 打印的cache_t中的_ocupied 為什麼是從2開始?

Cache_t底層原理分析

  • 透過源碼struct cache_t找到incrementOccupied()函數,查看incrementOccupied()具體實現
  • incrementOccupied()具體實現如下
  • 我們可以在全局環境中搜索incrementOccupied(),看看有哪些地方調用了incrementOccupied(),可以看到只有在cache_t的Insert有調用。
  • cache_t::insert方法為cache_t 的插入,cache_t 內部儲存的是sel-imp ,以下為從insert方法調用分析,如下為流程圖

insert方法分析

  • 以下為詳細註釋

主要分為三個流程區塊

  • 流程一:計算當前的緩存佔用量
  • 流程二:根據計算後的緩存佔用量進行不同的流程操作
  • 流程三:對需要儲存的bucket進行進行內部imp和sel賦值

流程一

mask_t newOccupied = occupied() + 1;

  • 沒有任何屬性被賦值以及無方法調用時,此時occupied為0,而newOccupied就變為1。
  • 有屬性被賦值時,會調用set方法,occupied會增加,當有多個屬性賦值時,就會依照此邏輯增加。
  • 有方法調用時,occupied也會增加,當有多個方法調用時,就會依照此邏輯增加。

流程二

  • 第一次創建,默認開闢4個(1左移兩位)。
if (slowpath(isConstantEmptyCache())) {
        //slowpath發生機率較小,即當occupied()=0時,及創建緩存,創建屬於發生機率較小的事件。
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        //初始化時,capacity = 4(1左移兩位)
        reallocate(oldCapacity, capacity, /* freeOld */false);
        //開闢空間
        //if 的流程為初始化創建
    }

  • 如果緩存佔用量小於等於3/4,則不做任何處理。
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.
        // 如果<=佔用內存的3/4,不做任何事情 確保沒有超過佔用內存的3/4
    }

  • 如果緩存佔用量超過3/4,則需要進行兩倍擴容以及重新開闢空間
else {
        //如果超出3/4 則需要擴容(兩倍擴容) 例如occupied為2時,剛好等於,不需擴容
        //擴容算法:有capacity,擴容兩倍,沒有capacity進行初始化為4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 4
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //滿了,重新梳理
        reallocate(oldCapacity, capacity, true);// 内存 擴容完毕
    }

reallocate方法:開闢空間

  • 第一次創建以及兩倍擴容時都會使用,源碼如圖所示。
  • allocateBuckets 方法:向系統開闢bucket,此時的bucket只是一個臨時變量

  • setBucketsAndMask 方法:將臨時的bucket存入緩存中,此時的儲存分為為兩種情況:

    • 如果是真機,根據bucketmask的位置儲存,並將occupied佔用設置為0

    • 如果不是真機,正常儲存bucketmask 並將occupied佔用設置為0

    • 如果有舊的buckets,需要清理之前的緩存,即調用cache_collect_free方法,其源碼實現如下

    cache_collect_free方法實現如下:

    • _garbage_make_room :創建垃圾回收桶

      • 如果是第一次,需要分配回收空間
      • 如果不是第一次 ,則將內存段加大,即原有的兩倍
      • 紀錄存儲這次的bucket
    • cache_collect 方法:垃圾回收,清理舊的bucket

    流程三

    主要根據cache_hash ****方法,哈希算法,計算sel-imp 存儲的下標,有以下幾種狀況

    • 如果哈希下標的位置未存儲sel ,表示該下標位置獲取sel等於0,將此時sel-imp存儲進去,並將occupied佔用加1
    • 如果當前哈希下標存儲sel等於即將插入的sel則直接返回。
    • 如果當前哈希下標存儲的sel不等於即將插入的sel,則重新通過cache_next方法即哈希衝突算法,重新進行哈希計算,得到新的下標,再去對比進行存儲。

    其中的哈希算法哈希衝突算法

    • cache_hash:哈希算法
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask; // 通过sel & mask(mask = cap -1)
    }
    
    
    • cache_next:哈希衝突算法
    #if __arm__  ||  __x86_64__  ||  __i386__
    // objc_msgSend has few registers available.
    // Cache scan increments and wraps at special end-marking bucket.
    #define CACHE_END_MARKER 1
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask; //(将当前的哈希下标 +1) & mask,重新进行哈希计算,得到一个新的下标
    }#elif __arm64__
    // objc_msgSend has lots of registers available.
    // Cache scan decrements. No end marker needed.
    #define CACHE_END_MARKER 0
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask; //如果i是空,则为mask,mask = cap -1,如果不为空,则 i-1,向前插入sel-imp
    }
    
    
    • 以上為cache_t的原理分析

    答疑

    • _mask是什麼?

    _mask是指掩碼數據,用於在哈希算法另一哈希冲突算法中計算哈希下标,其中 mask = capacity - 1

    • _occupied是什麼?

    表示(hash-table)哈希表中sel-imp佔用大小 (分配內存中已經存儲了sel-imp的個數)

    • init會導致occupied變化

    • 属性赋值,也會隱式調用,導致occupied變化

    • 方法调用,導致_occupied變化

    • 為什麼通過方法調用的增加,其打印的_occupiedmask会变化

    因為在cache初始化時,分配的空間是4個,通過方法調用的增量,當存儲的sel-imp個数,即newOccupied + CACHE_END_MARKER(等於1)的和 超过 總容量的3/4,例如當occupied等於2時,newOccupied就等於3上述總和為4,就需要對cache的內存進行兩倍擴容

    • bucket數據為什麼丢失的情况會有?,例如2-7中,只有say3,say4方法有函數指針

    原因是在擴容時,是將原有的內存全部清除了,再重新申請内存導致的。

    • 為什麼是say4先打印,say3後打印,明顯順序有問題?

    因為sel-imp的存儲是通過哈希算法計算下標的,其計算的下標有可能已經存儲了sel,所以又需要通過哈希衝突重新計算哈希下標,所以導致下標是隨機的,並不是固定的。

    • 打印的cache_t中的_ocupied為什麼是從2開始?

    這裡是因為LGPerson通過alloc創建的對象,並導致兩個屬性賦值的原因,屬性賦值,會隐式调用set方法,set方法的調用也會導致occupied變化。

流程圖

你可能感兴趣的:(cache原理分析)