iOS-類(NSObject)的方法緩存

上一篇文章中大概的分析了類的結構、以及類的屬性與方法的存儲,接下來咱們分析類結構中的方法緩存:cache。數組

類的結構:緩存

truct 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

。。。
}
複製代碼

1、cache介紹

一、cache的做用

類的方法緩存,增長方法查找效率。bash

二、cache_t內部結構:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

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(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
複製代碼
  • _buckets:bucket_t結構體的數組,bucket_t是用來存放方法的SEL內存地址和IMP的
  • _mask:數組大小 - 1,用做掩碼。
  • _occupied:當前已緩存的方法數。
struct bucket_t {
    cache_key_t _key; // typedef unsigned long
    IMP _imp;
...
};
複製代碼
  • _key:cache_key_t 就是 unsigned long類型,用來存儲SEL的內存地址。SEL應該是char類型的字符串,char強轉unsigned long,其實就是SEL的內存地址。
  • _imp:方法對應的函數內存地址。

2、cache_t結構中的主要函數

一、緩存查找

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

// --------------------find內部調用的cache_hash-------------------------
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
// --------------------find內部調用的cache_next(arm64)-----------------
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
複製代碼

分析:less

  • 獲取到buckets散列表與掩碼mask,經過cache_hash(key & mask)獲得key值對應的索引begin。函數

  • 將begin賦值給i,方便切換索引。post

  • 進行do-while循環查找ui

    (i = cache_next(i, m)) != begin
    複製代碼

    好比起始下標是4, 總長度是8,依次遞減,當i = 0時,返回mask,則繼續從總長度 = 8開始查找,直到index 再次等於 4,說明已經遍歷一圈了,仍是沒找到,方法緩存查找結束。this

    if (b[i].key() == 0  ||  b[i].key() == k)
    複製代碼

    用i做爲索引從散列表中取值,若是取出來的bucket_t的 key = k,則查詢成功,返回該bucket_t。spa

    若是key = 0,說明在索引i的位置上尚未緩存過方法,一樣須要返回該bucket_t,用於終止緩存查詢。線程

  • 若是此時尚未找到key對應的bucket_t,或者是空的bucket_t,則循環結束,說明查找失敗,調用bad_cache方法。

二、緩存填充插入

緩存填充插入函數中調用了查找方法,這個方法也是咱們須要關注的重點

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 was not added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // 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(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}
//-------------------------capacity() -----------------------------
mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}
複製代碼
  • 在確保緩存沒有被其餘線程添加

  • 記錄對當前將要緩存的方法數mask_t newOccupied = cache->occupied() + 1;

  • 獲取當前散列表的容量

  • 判斷當前cache是否爲只讀的,也就是還未初始化,則從新分配緩存容量

  • 若是newOccupied大於容量的3/4,就要進行擴容

  • 經過find函數獲取到對應的bucket_t。

  • 判斷返回的bucket->key() 是否爲0

    若是爲0,就說明該位置上是空的,沒有緩存過方法,所以須要添加一個新的緩存。

    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }
    複製代碼
  • 將key與imp保存在緩存中

三、緩存擴容

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 not grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}

//--------------------------------------------------------
INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2) // 4
複製代碼

當前緩存容量不存在時,開闢一個INIT_CACHE_SIZE(4)大小的緩存容量,存在則*2翻倍。

四、緩存分配

能夠看到,緩存填充插入與擴容都須要用到緩存分配函數reallocate:

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

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's 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 cache_t::canBeFreed()
{
    return !isConstantEmptyCache();
}

bool cache_t::isConstantEmptyCache()
{
    return 
        occupied() == 0  &&  
        buckets() == emptyBucketsForCapacity(capacity(), false);
}

//---------------------------setBucketsAndMask-------------------------------
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    mega_barrier();

    _buckets = newBuckets;
    _mask = newMask;
    _occupied = 0;
}
複製代碼
  • 判斷是否須要釋放

    canBeFreed函數用來判斷當前緩存是否爲空,若是爲空,也就沒有必要釋放了。

  • 根據容量newCapacity分配新的buckets

  • 爲cache_t中的各個參數從新賦值,這塊是指向了一個新的內存空間

  • 根據第一步的判斷釋放緩存

3、總結

調用方法的緩存會以一個哈希散列表bucket_t的形式保存在類結構中的cache中,cache的緩存容量不存在時,開闢一個INIT_CACHE_SIZE(4)大小的緩存容量,緩存超過容量3/4時,會進行擴容*2,開闢一個新的內存空間,並釋放以前舊的緩存空間,同時保存此次的調用方法緩存。

相關文章
相關標籤/搜索