iOS 底層探索篇 —— cache_t分析

前言算法

觀察cache_t結構體

1. 類結構體

struct objc_class : objc_object {
    // Class ISA;           //8
    Class superclass;       //8
    cache_t cache;          //16        // formerly cache pointer and vtable
    class_data_bits_t bits;  
...省略部分信息...
};
複製代碼

2. cache_t結構體

struct cache_t {
    struct bucket_t *_buckets;  //8
    mask_t _mask;               //4      typedef uint32_t mask_t;  
    mask_t _occupied;           //4

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(SEL sel, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
複製代碼

3. struct bucket_t *_buckets

struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;   //typedef unsigned long           uintptr_t;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
    ...省略部分信息...
};
複製代碼
  • _buckets一個指向bucket_t結構體的指針,能夠看命名是一個數組;
  • _imp函數實現地址;
  • _sel函數;
  • 注意在arm64的平臺上_imp_sel的前面。

4. mask_t _mask

一個修飾的值,在接下來的分析能夠獲得是 hash鏈表的長度減1。數組

5. mask_t _occupied

緩存已佔用的空間緩存

cache_t緩存的內容

經過bucket_t結構體觀察存的就是impsel,證明緩存的是OC的方法。bash

cache_t緩存的流程

1. 緩存入口

咱們在objc的源碼objc_cache.mm文件的開始位置能夠看到這麼一段註釋less

* Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache writers (hold cacheUpdateLock while reading or writing; not PC-checked)
 * cache_fill         (acquires lock)
 * cache_expand       (only called from cache_fill)
 * cache_create       (only called from cache_expand)
 * bcopy               (only called from instrumented cache_expand)
 * flush_caches        (acquires lock)
 * cache_flush        (only called from cache_fill and flush_caches)
 * cache_collect_free (only called from cache_expand and cache_flush)
複製代碼

既然是緩存就是要寫入,咱們直接從Cache writers下面看,cache_fill就是緩存開始的位置。函數

2. cache_fill

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}
複製代碼

忽略其餘信息,只有cache_fill_nolock這個函數是咱們的目標。post

3. cache_fill_nolock

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

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

    // Make sure the entry wasnot added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
       // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(sel, receiver);
    if (bucket->sel() == 0) cache->incrementOccupied();
    bucket->set<Atomic>(sel, imp);
}
複製代碼

cache_t緩存的流程深刻

從上面的分析咱們知道在函數cache_fill_nolock裏面會實現緩存步驟。接下來開始對每一步深刻研究。ui

1. 緩存主線流程分析

1. 條件判斷

if (!cls->isInitialized()) return;
 if (cache_getImp(cls, sel)) return;
複製代碼
  • 判斷類是否已經初始化,尚未就直接返回了。
  • 在類裏面是否能夠找到sel,能夠找到,說明已經緩存了,直接返回。

2. cache_t *cache = getCache(cls)

cache_t *getCache(Class cls) 
{
    assert(cls);
    return &cls->cache;
}
複製代碼

從當前類獲取cache成員。this

3. mask_t newOccupied = cache->occupied() + 1

  • occupied()函數返回的是_occupied,即當前已經佔用的空間大小。
  • _occupied + 1 , 表示新的即將佔用的內存空間大小。

4. mask_t capacity = cache->capacity()

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
    //mask() return _mask;
}
複製代碼

獲取緩存的容量,注意這裏 + 1,緣由是由於賦值的時候,用了容量 - 1。atom

5. 緩存容量的處理

if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        //INIT_CACHE_SIZE 4
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }
複製代碼
  • cache是空的,就須要reallocte
  • newOccupied小於或者等於總容量的3/4,就走後面的。
  • 若是大於3/4,就須要expand()即緩存擴容。

6. 查找bucket以及賦值

bucket_t *bucket = cache->find(sel, receiver);
if (bucket->sel() == 0) cache->incrementOccupied();
bucket->set<Atomic>(sel, imp);
複製代碼

緩存的主線流程就是這樣的一個過程的,接下來把第5步和第6步進行分析。

2. 緩存主線流程 容量處理過程查找過程分析

1. 容量處理過程

  1. cache->isConstantEmptyCache()
bool cache_t::isConstantEmptyCache()
{
    return 
        occupied() == 0  &&  
        buckets() == emptyBucketsForCapacity(capacity(), false);
}
複製代碼
  • 第一次進來的時候occupied()返回值_occupoed值爲0。
  • buckets()返回值_buckets是一個尚未初始化的指針爲nil。
bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true)
{
    cacheUpdateLock.assertLocked();

    size_t bytes = cache_t::bytesForCapacity(capacity);
   
    // Use _objc_empty_cache if the buckets is small enough.

    if (bytes <= EMPTY_BYTES) {
        return (bucket_t *)&_objc_empty_cache;
    }
    
    *
    走到這個就會直接返回了
    bucket_t *          8                          1000
    _objc_empty_cache   16                        10000
                        &                         00000
    *
    
    //下面的代碼與咱們的流程沒有關係 暫不分析
    // Use shared empty buckets allocated on the heap.
    static bucket_t **emptyBucketsList = nil;
    static mask_t emptyBucketsListCount = 0;
    
    mask_t index = log2u(capacity);

    if (index >= emptyBucketsListCount) {
        if (!allocate) return nil;

        mask_t newListCount = index + 1;
        bucket_t *newBuckets = (bucket_t *)calloc(bytes, 1);
        emptyBucketsList = (bucket_t**)
            realloc(emptyBucketsList, newListCount * sizeof(bucket_t *));
        // Share newBuckets for every un-allocated size smaller than index.
        // The array is therefore always fully populated.
        for (mask_t i = emptyBucketsListCount; i < newListCount; i++) {
            emptyBucketsList[i] = newBuckets;
        }
        emptyBucketsListCount = newListCount;

        if (PrintCaches) {
            _objc_inform("CACHES: new empty buckets at %p (capacity %zu)", 
                         newBuckets, (size_t)capacity);
        }
    }

    return emptyBucketsList[index];
}
複製代碼
  • cache_t::bytesForCapacity(capacity),參數capacity爲0,實現爲sizeof(bucket_t) * (cap + 1)bucket_t結構體裏面有兩個成員都是8字節總共16字節,運算結果爲16。
  • EMPTY_BYTES,一個宏定義,在DEBUG值爲(8+1)*16,不然爲(1024+1)*16
  • if (bytes <= EMPTY_BYTES)顯然這個條件是知足的。
  • (bucket_t *)&_objc_empty_cache,這是一個&運算,OBJC_EXPORT struct objc_cache _objc_empty_cache這是_objc_empty_cache的定義,它的字節佔用的值爲16,咱們在objc_cache這個結構體裏面是能夠看到的。 8 & 16 記過計算出來爲0。
  • if條件裏面返回了,後面的流程就不用看了。

則第一次進來的時候這個條件判斷是否爲空的緩存是存在的。

  1. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE)

表達的意思爲緩存空的時候,就去開闢。

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();
   
    bucket_t *oldBuckets = buckets();

    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache is old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
  
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
複製代碼
  • bool freeOld = canBeFreed(),函數的實現就是!isConstantEmptyCache(),則freeOld的值爲true
  • bucket_t *newBuckets = allocateBuckets(newCapacity),根據容量 系統開闢一個新的buckets
  • setBucketsAndMask(newBuckets, newCapacity - 1)。作了三件事,空的_buckets賦值;_mask賦值,注意這裏是容量 - 1;_occupied已佔用的空間置爲空。
  • if (freeOld)經過上面的取值以及知道是成立的,主要作了清空舊的緩存。
  1. newOccupied <= capacity / 4 * 3

這個意思就是新佔用的空間是否小於總容量的3/4。

  1. cache->expand()

同比第7步,若是容量大於3/4的時候,內存須要擴容。

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can it grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}
複製代碼
  • uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;,這裏擴容後的容量會是舊容量的兩倍。
  • reallocate(oldCapacity, newCapacity),這一步如同剛開始沒有緩存的時候,從新讓系統來開闢。

2. 查找過程

  1. bucket_t *bucket = cache->find(sel, receiver)
bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);

    bucket_t *b = buckets(); //獲取_buckets
    mask_t m = mask(); //獲取_mask

    mask_t begin = cache_hash(s, m);
    mask_t i = begin;
    do {
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin); 
    
    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}
複製代碼
  • cache_hash(sel, _mask),這是一個hash算法return (mask_t)(uintptr_t)sel & mask,這裏也能夠知道_mask會比總容量小1的緣由,就是這裏計算的時候保證不會越界。
  • do...while循環,當_sel不存在或者在緩存裏面匹配到了,就返回當前的bucket。循環的條件在arm64下面是return i ? i-1 : mask;,實際上就是造成一個閉環來查找。
  1. if (bucket->sel() == 0) cache->incrementOccupied()
void cache_t::incrementOccupied() 
{
    _occupied++;
}
複製代碼

表示在緩存裏面沒有找到的時候,這個時候要存到緩存裏面,則已佔用的空間須要增長。

  1. bucket->set<Atomic>(sel, imp)
void bucket_t::set(SEL newSel, IMP newImp)
{
    _imp = (uintptr_t)newImp;
    
    if (_sel != newSel) {
        if (atomicity == Atomic) {
            mega_barrier();
        }
        _sel = newSel;
    }
}
複製代碼

這一步就是表示當前取出來的bucket來從新賦值_sel_imp這兩個成員。

到此爲止cache的緩存流程就結束了。

相關文章
相關標籤/搜索