iOS底層學習 - 類的前世此生(二)

經過上一個章節,咱們已經知道的類在底層是以什麼樣的方式存在的,而且類的屬性,成員變量和方法的存儲也有了必定的瞭解,可是類的方法是怎麼讀取的,每次都要從存儲的列表中讀出來麼,是否是又必定的緩存機制呢?咱們開始研究算法

傳送門☞iOS底層學習 -類的前世此生(一)數組

cache_t結構

經過查看類的結構,咱們知道isa是用來指向類信息的,superclass是父類相關,class_data_bits_t是用來存儲屬性,方法等數據的,那麼若是有緩存機制的話,必定是存儲在cache_t中了緩存

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;  
...省略方法等信息...
};
複製代碼

經過上一章節,咱們對cache_t有個初步的瞭解,結構如圖 安全

cache_t功能

cache_t的底層是一個哈希表存在,用於緩存調用過的方法,提升查找速度,不用每次從class_data_bits_t進行遍歷查找。用哈希表存儲時,存儲的位置是不肯定的,空間也有必定的浪費,可是時間複雜度比較低,是典型的空間換時間bash

cache_t定義

struct bucket_t *_buckets

struct bucket_t *_buckets是一個結構體指針less

  • cache_key_t爲方法的SEL,也就是方法名,
  • MethodCacheIMP爲對應的函數的內存地址

struct bucket_t * find(cache_key_t key, id receiver);方法能夠得出,cache_t底層的存儲是一個以cache_key_t爲key,bucket_t爲value的一個哈希表函數

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__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};
複製代碼

mask_t _mask

mask_t _mask只是一個32位的int值 ,等於(哈希表長度 - 1)post

typedef uint32_t mask_t;
複製代碼

mask_t _occupied

同理_occupied也是一個值,記錄了緩存的方法的數量學習

cache_t流程

經過對objc_cache.mm源碼的註釋的閱讀,咱們能夠獲得一個緩存讀寫的大體過程。相關讀取的過程,即在方法轉發過程當中,獲取到已緩存的IMP函數指針,從而得到方法 ,重點在存取的過程,能夠從方法cache_fill開始ui

cache_fill

經過註釋咱們得知,存取的過程是須要加鎖來保證線程安全的,_collecting_in_critical相似輪詢線程,保證調用,因此,主要實現的主要方法再cache_fill_nolock(cls, sel, imp, receiver);中進行

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
}
複製代碼

locker構造時加鎖,析構時解鎖,正好保護了方法做用域內的方法調用。這和 EasyReact 中大量使用的__attribute__((cleanup(AnyFUNC), unused))一模一樣,都是爲了實現自動解鎖的效果。

class locker : nocopy_t {
        mutex_tt& lock;
    public:
        locker(mutex_tt& newLock) 
            : lock(newLock) { lock.lock(); }
        ~locker() { lock.unlock(); }
    };
複製代碼

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

複製代碼

經過上面的源碼,咱們能夠得出如下主要步驟

  • if (!cls->isInitialized()) return;若是類沒有進行初始化操做,則不能進行緩存的操做,這個比較好理解
  • if (cache_getImp(cls, sel)) return;由於有可能其餘線程先進行了存儲,因此須要再找查找一遍,若是能夠找到緩存,則直接返回,不須要進行緩存的存儲
  • cache_t *cache = getCache(cls);cache_key_t key = getKey(sel);分別爲獲取到類的cache_t對象和根據方法名獲取到cache_key_t對象
  • mask_t newOccupied = cache->occupied() + 1;mask_t capacity = cache->capacity();分別爲cache對象的Occupied和mask對象在原基礎上+1
  • if (cache->isConstantEmptyCache())表示cache是隻讀的,此時,須要執行cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);方法進行從新申請內存
  • else if (newOccupied <= capacity / 4 * 3)沒有超出哈希表3/4容量時,跳過直接進行下面緩存的操做
  • 若是超出哈希表3/4容量時,須要執行cache->expand();進行哈希表擴容
  • bucket_t *bucket = cache->find(key, receiver);根據key進行方法存儲
  • cache->incrementOccupied()Occupied++
  • bucket->set(key, imp);寫入哈希表

經過上面的分析,咱們對cache的存儲流程有了大致的瞭解,其中重點的流程在於緩存如何申請空間cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE),如何擴容cache->expand();,如何寫入緩存bucket_t *bucket = cache->find(key, receiver);

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

首先是isConstantEmptyCache()方法,表示buckets是一個只讀數組。主要邏輯以下

  • occupied是否爲空,即_occupied值是否爲0;
  • 根據傳入的capacity計算大小,若是小於EMPTY_BYTES,則直接返回(bucket_t *)&_objc_empty_cache,二進制運算後爲空
  • 因此這方法基本就表示此時空間尚未初始化,因此須要初始化
bool cache_t::isConstantEmptyCache()
{
    return 
    occupied() == 0  &&  
    buckets() == emptyBucketsForCapacity(capacity(), false);
}

複製代碼
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;
    }

    // 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::reallocate方法,這個方法主要是用來申請緩存空間,主要邏輯以下

  • canBeFreed()表示緩存空間不爲空,若是爲空則不須要後續的清空操做
  • bucket_t *oldBuckets = buckets();獲取舊的緩存空間,bucket_t *newBuckets = allocateBuckets(newCapacity);是指根據傳入的空間,生成新的緩存空間,初始值爲INIT_CACHE_SIZE4字節
  • setBucketsAndMask(newBuckets, newCapacity - 1);設置cache_t中的屬性
  • cache_collect_free(oldBuckets, oldCapacity);釋放舊的緩存空間,在新的緩存空間進行緩存
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();

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

    // Cache is not 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);
    }
}
複製代碼

cache->expand()

這個方法就是判斷若是此時存儲大於了緩存空間的3/4時,對緩存空間進行擴容,算法也比較簡單粗暴,就是以前緩存空間的2倍大小,完成後調用reallocate生成空間

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);
}
複製代碼

bucket_t *bucket = cache->find(key, receiver);

這個方法就是根據key找到底層哈希表存儲的對應的bucket_t,主要流程以下

  • cache_hash 經過cache_hash函數,即key&mask計算出key值對應的index值 begin,用來記錄查詢起始索引
  • do while循環表示用這個i從散列表取值,若是取出來的bucket_t的 key = k,則查詢成功,返回該bucket_t,若是key = 0,說明在索引i的位置上尚未緩存過方法,一樣須要返回該bucket_t,用於停止緩存查詢。
  • while至關於 i = i-1,回到上面do循環裏面,至關於查找散列表上一個單元格里面的元素,再次進行key值k的比較,當i=0時,也就i指向散列表最首個元素索引的時候從新將mask賦值給i,使其指向散列表最後一個元素,從新開始反向遍歷散列表,其實就至關於繞圈,把散列表頭尾連起來,不就是一個圈嘛,從begin值開始,遞減索引值,當走過一圈以後,必然會從新回到begin值,若是此時尚未找到key對應的bucket_t,或者是空的bucket_t,則循環結束,說明查找失敗,調用bad_cache方法。
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);
    // begin 賦值給 i,用於切換索引
    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);
}
複製代碼
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
複製代碼
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
複製代碼

至此,一個cache的基本流程就完成了

總結

  • 當方法調用須要被緩存時,以cache_key_t_keyMethodCacheIMP的方式緩存在類的_buckets中,初始是一個4字節的哈希表,mask值爲哈希表長度-1。存儲時,使用SEL轉換爲的cache_key_t_key&mask來當作下標存入哈希表
  • 當存儲控件大於哈希表容量3/4時,會進行擴容,擴容會清空以前因此緩存,並生成以前緩存空間2倍的新空間進行從新緩存

參考

iOS 底層拾遺:objc_msgSend 與方法緩存

Runtime筆記(三)—— OC Class的方法緩存cache_t

相關文章
相關標籤/搜索