objc_class(cache探索)

在objc_class結構體中,有一個cache_t類型,在這篇內容中,主要探索這個內容存儲的數據,以及存儲的規(guī)律。老樣子通過內存位移先得到cache_t的內存地址。


cache_t的打印結果,看起來沒有特別清楚的字段,去cache_t結構體里看看

在源碼中,沒具體找到相關信息,但是看到了insert方法,

我猜測插入方法,應該就是加入相應的方法緩存,因為有sel和imp,我們可以根據這個insert方法進行追蹤,點進去看實現找到了一個buckts,方法內部就是操作這個buckts。


buckts

而cache_t有一個buckets()方法,進行打印
buckets打印結果

老樣子,暫時看不出什么有用的信息,就去bucke_t結構體里面去瞧瞧

看一下這個capacity

unsigned oldCapacity = capacity(), capacity = oldCapacity;
capacity方法實現

繼續(xù)跳下去


mask方法

那么_maybeMask = capacity - 1

那我們就繼續(xù)打印一下buckts方法,看看里面的內容。


打印buckets方法

bucket_t中的sel和imp方法

繼續(xù)打印


sel和imp

老規(guī)矩,看到不太明白的數據直接去源碼中查找有沒有對應的方法
找到了對應的sel和imp

我們可以通過查看sel方法來得到當前bucket_t指針數組中的存儲內容,因為是一個指針數組類型,所以我們可以通過地址+1的方法,獲取所有的存儲內容.因為是散列表存儲的,所以可能不連續(xù),所以第一個+1的地址沒有存儲方法。
打印

打印

可以通過打印結果來看,bucket里面存儲了一個class方法和一個respondsToSelector方法,既然是緩存方法,那么再調用方法之后,應該也可以打印出來,調用method1。


method1之后打印緩存

從圖片來看打印結果,method1不僅沒有出現,respondsToSelector也消失不見了。這就是因為緩存擴容的原因,舊的bucket被釋放了,儲存的數據也釋放了。我們來探究一下cache擴容。

cache擴容

在insert方法中,如果緩存超出一定的臨界值,就會進行擴容。


cache擴容方法

occupied()方法

可以看出返回的是cache_t的成員變量_occupied,那么首次,_occupied=0,即newOccupied = 1;

//capacity方法實現
unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

//mask方法實現
mask_t cache_t::mask() const
{//_maybeMask = bucket_t的長度-1
    return _maybeMask.load(memory_order_relaxed);
}

oldCapacity = capacity();capacity = _maybeMask + 1;而 _maybeMask = bucket長度- 1;即capacity = bucket長度。

  • 反過來看第一個條件判斷
if (slowpath(isConstantEmptyCache())) {//當緩存為空的時候
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//賦一個INIT_CACHE_SIZE
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }

//跳一下 INIT_CACHE_SIZE
enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
    // When we have a cache end marker it fills a bucket slot, so having a
    // initial cache size of 2 buckets would not be efficient when one of the
    // slots is always filled with the end marker. So start with a cache size
    // 4 buckets.
    INIT_CACHE_SIZE_LOG2 = 2,//
#else
    // Allow an initial bucket size of 2 buckets, since a large number of
    // classes, especially metaclasses, have very few imps, and we support
    // the ability to fill 100% of the cache before resizing.
    INIT_CACHE_SIZE_LOG2 = 1,
#endif
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
    FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
    FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};

//

MAX_CACHE_SIZE_LOG2的值依賴于CACHE_END_MARKER,而CACHE_END_MARKER:在x86_64下位1,在arm64下值為0,所以MAX_CACHE_SIZE_LOG2在x86_64下位2,在arm64下為1
INIT_CACHE_SIZE在x86_64下1<<2 = 4;在arm64下1<<1為2,即bucket_t初始化的長度,在arm64家溝下位2,在x86_64下為4;初始長度計算結束后,然后調用了reallocate(oldCapacity, capacity, false); //false是是否釋放掉舊的bucket
reallocate實現

ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    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) {
        collect_free(oldBuckets, oldCapacity);//第一次沒有舊的buckets,所以不用釋放
    }
}

那么這個條件判斷,我們就可以理解為在arm64架構下開辟一個長度為2的bucket,在x86_64架構下開辟一個長度為4的bucket。

  • 再看第二個條件判斷
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }

//cache_fill_ratio方法如下圖
cache_fill_ratio

由圖可得,在x86_64架構下為bucket長度的四分之三,而CACHE_END_MARKER在x86_64下=1,計算基數會+1,即長度為4的情況下,第三個方法就會存進新的筒子里;
在arm64架構下為bucket長度的八分之七,即當長度為16的時候,第十五個數據會進行擴容。
也就是這個條件判斷可以理解為,在arm64架構下,如果緩存的大小小于等于bucket長度的八分之七,在x86_64架構下如果緩存的大小小于等于bucket長度的四分之三,則什么也不干。

  • 第三個條件
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
FULL_UTILIZATION_CACHE_SIZE

FULL_UTILIZATION_CACHE_SIZE 等于1<<3=8
即在arm64架構下,當筒子的長度小于等于8的時候,什么都不做

  • 最后擴容寫法
else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

上述條件都不滿足的時候,直接兩倍擴容,最大值是MAX_CACHE_SIZE = 1<<16,最后reallocate傳的是true,就是將舊bucket釋放掉,原來在舊bucket存的值就不存在了

整個cache擴容的規(guī)則

  • 在x86_64架構下,當緩存的大小等于bucket長度的四分之三的時候(會進行+1),進行兩倍擴容
  • 在arm64架構下,當緩存的大小小于等于bucket長度的半分之七的時候,進行兩倍擴容,當bucket長度小于等于8的時候,不滿不擴容
  • 擴容后,釋放舊bucket,新bucket不存儲舊bucket內容

那根據這個擴容規(guī)則,電腦架構是x86,開頭遇見的問題在執(zhí)行method1方法后,bucket里面只有一個class,是因為在執(zhí)行method的時候,進行了擴容,所以只有一個class方法。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容