Cache_t 分析

類結構分析中 知道了屬性和方法是如何獲取的,可是爲了看 class_data_bits_t bits; 中的內容,僅僅計算了 cache_t cache; 的大小,並無認真的瞭解和研究過 cache_t ,當前內容就是對 Cache_t 的分析。緩存

一、認識 cache_t

cache_t 是對 OC 程序中使用過方法的緩存,以便於下次調用方便。每次調用方法以前都須要先從 cache_t 中查找,若是有緩存,就不須要通過漫長的方法查找而直接調用了,若是沒有就給 cache_t 中緩存一份。bash

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    //...
}


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__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif

    //...

}

複製代碼

上方代碼就是 struct cache_t 中全部的成員,結構體中 _bucketsbucket_t * 的結構體指針類型, 存儲的是緩存方法的 SEL 方法編號和 IMP 方法實現的指針地址;_mask 表明的是當前能緩存的方法的個數;_occupied 表明的是當前已經緩存的方法數。less

二、LLDB 打印 cache_t

既然知道 cache_t 成員所表明的含義,那如今經過 lldb 調試一下,main.m 文件中,寫入下方代碼。post

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        TestClass *object = [TestClass alloc];
        Class pClass = object_getClass(object);
        NSLog(@"%@ - %p",object,pClass);
    }
    return 0;
}

複製代碼

lldb 調試結果以下:ui

類結構分析中 中說過:執行 x/4gx 後,第一個 8 字節表明 isa ,第二個 8 字節表明 superClass,因此首地址偏移 16 個字節就是 cache_t 的首地址,一系列的取值後發現是空的,沒有緩存,那明明調用了 alloc 方法了啊,就算 alloc 方法特殊,可是也是方法啊,怎麼沒有緩存呢?this

緣由是:alloc 是類方法,類方法存儲在當前類的元類裏面,類裏面存儲的是實例方法。spa

明白了上述緣由,嘗試添加實例方法再次打印。線程

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        TestClass *object = [TestClass alloc];
        Class pClass = object_getClass(object);
        [object testClassInstanceMethod];
        NSLog(@"%@ - %p",object,pClass);   
    }
    return 0;
}
複製代碼

打印結果以下:3d

上方截圖打印出來發現,_mask 爲 3,_occupied 爲 1,打印出了上方代碼調用過的 testClassInstanceMethod 了,發現確實類的 cache_t 只是緩存的類的實例方法,而且調用一次後就會緩存。指針

三、自定義輸出 cache_t

每次都使用上方 lldb 都累的夠嗆,當前使用一個自定義的方式去打印一下。自定義代碼以下:

typedef unsigned long           uintptr_t;
typedef uint32_t mask_t;

struct custom_bucket_t {
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
};

struct cache_t {
    struct custom_bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

struct custom_class_data_bits_t {
    
    // Values are the FAST_ flags above.
    uintptr_t bits;
};

struct custom_objc_class  {
    Class ISA;
    Class superclass;
    struct cache_t cache;             // formerly cache pointer and vtable
    struct custom_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        TestClass *object = [TestClass alloc];
        Class pClass = object_getClass(object);
        
        [object testClassInstanceMethod];
        [object testClassInstanceMethod_1];
        [object testClassInstanceMethod_2];
        
        struct custom_objc_class *custom_pClass = (__bridge struct custom_objc_class *)(pClass);
      
        for (mask_t i = 0; i < custom_pClass->cache._mask; i++) {
            struct custom_bucket_t bucket = custom_pClass->cache._buckets[i];
            NSLog(@"%s ---- %lu",bucket._sel,bucket._imp);
        }
        
        NSLog(@"結束打印:%@ - %p",object,pClass);
    }
    return 0;
}
複製代碼

打印結果以下:

這樣就能很清晰的看到 cache_t 中的緩存方法。

四、多個方法調用自定義輸出 cache_t

上方代碼調用了3個實例方法,那咱們調用4個或者多個呢?上方代碼再添加一個調用 [object testClassInstanceMethod_3]; 打印結果以下:

發現居然只有一個緩存方法打印了,其餘的呢?還有這裏調用 4 個方法的打印了 7 條,上個調用 3 個方法的就打印了 3 條呢?

帶着疑問接着看 cache_t, 以前都是隻看了成員是什麼?成員的含義是什麼?可是沒有看過這些值是怎麼賦值的吧。

五、對 cache_t 結構體中的方法追蹤

隨意個 cache_tmask_t cache_t::mask() 方法打個斷點 ,而後再在咱們調用方法前打個斷點,先讓程序走到咱們調用方法以前,不然就會不斷的進入系統方法的調用。

程序進入了 mask() 的斷點,看下調用隊棧信息:

從上方隊棧信息能看到,先開始查找方法,而後緩存方法,這裏只關注方法緩存,看到關於緩存的字樣 cache_fill() 繼續上述操做,斷點進入 cache_fill(),再下一步進入 cache_fill_nolock(),發現此時纔是當前 cache_t 的重頭戲。

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_t 緩存源碼 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.
    //確保在獲取cacheUpdateLock以前,條目沒有被其餘線程添加到緩存中。

    
    //若是緩存中找到了,就 return 什麼也不須要幹
    if (cache_getImp(cls, sel)) return;

    //取出類對象中的 cache_t
    cache_t *cache = getCache(cls);

    // Use the cache as-is if it is less than 3/4 full
    //若是緩存不足3/4,則按原樣使用緩存
    
    //獲取當前保存的緩存已經使用的大小,而後進行 + 1 ,由於當前須要緩存新的。
    mask_t newOccupied = cache->occupied() + 1;
    
    //獲取當前 cache 的最大容量 _mask + 1
    mask_t capacity = cache->capacity();
    
    //若是是一個只讀的空緩存就進入
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        //緩存是隻讀的。取代它
        
        //開闢一個緩存空間 capacity > 0 ? capacity : 4
        // INIT_CACHE_SIZE : (1 << INIT_CACHE_SIZE_LOG2) -----> 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.
        //緩存小於3/4滿。按原樣使用它。
    }
    else {
        // Cache is too full. Expand it.
        
        //超出了緩存大小的 4分之3 就須要 擴容
        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.
    
    //掃描第一個未使用的插槽並插入。
    //保證有一個空槽,由於最小尺寸是4,咱們調整了3/4滿的尺寸。
    
    //哈希去查找
    bucket_t *bucket = cache->find(sel, receiver);
    
    if (bucket->sel() == 0) {
        // 若是 _sel 沒找到
        /**
         *  incrementOccupied() {
         *    //當前已經緩存的方法總數+1;
         *     _occupied++;
         *  }
         */
        cache->incrementOccupied();
    }
        
    //將當前要調用方法緩存
    bucket->set<Atomic>(sel, imp);
}
複製代碼

七、對 cache_t 緩存源碼 expand 擴容的分析

void cache_t::expand()
{
    //鎖
    cacheUpdateLock.assertLocked();
    
    //獲取原來緩存的最大容量
    uint32_t oldCapacity = capacity();
    
    //若是原來緩存的最大容量 > 0,就在原來的容量上 * 2,不然就返回 4
    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);
}

複製代碼

八、對 cache_t 緩存源碼 reallocate 的分析

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

    //給cache 從新賦值緩存池 _buckets,能存儲的_mask = newCapacity - 1 的值,而後把當前已經緩存的方法個數置爲0
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    //若是能夠釋放就釋放掉舊的緩存池
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
複製代碼

以上就是對 cache_t 的緩存方法的流程了。

相關文章
相關標籤/搜索