iOS方法緩存cache_t流程-海浪寶寶

先放一份流程圖:

cache流程圖.png

從上帝視角,我們知道緩存方法的入口是cache_fill_nolock,所以我們先找到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 wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    //如果能取到方法指針,直接返回,看不到源碼
    if (cache_getImp(cls, sel)) return;
    //取到緩存池
    cache_t *cache = getCache(cls);
    //將方法強轉成long類型,當做key使用,這里我們可以看到緩存池是將SEL轉成一個long類型來當做key使用
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    //取到當前緩存池的緩存方法數并+1
    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);
    }//再加一個緩存,如果緩存池的緩存的方法數小于緩存池容量的4/3則不管,如果大于則擴容
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {//如果緩存的方法大于容量的4/3,則擴容,擴容成現有緩存池總容量的2倍
        // 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
    bucket_t *bucket = cache->find(key, receiver);
    //如果取到的bucket的key為0說明沒從緩存池中取到改方法,那么就將緩存池已緩存的數量+1
    if (bucket->key() == 0) cache->incrementOccupied();
    //將執行的方法緩存的緩存池中
    bucket->set(key, imp);
}

結論:

里面我們可以看到蘋果的緩存邏輯是當新方法進來之前判斷加入后緩存的值是否大于總容量的4/3,如果大于則將緩存池擴容成老緩存池的二倍,這個緩存思想比較好,不會隨便建立大的緩存池,也不會導致不夠用,

擴容方法

//擴容方法,將容量擴充到原來的2倍,如果為空的話則默認給個4
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    //新的容量值強轉成uint32_t,和原來的不相等就用老的容量值?
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    //創建新的容量池,創建完成后會將原來緩存的方法清除掉
    reallocate(oldCapacity, newCapacity);
}

開辟緩存池

開辟緩存池方法

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    //如果緩存池容量為0,且緩存的方法為空,則不能清空
    bool freeOld = canBeFreed();
    //q取到老的緩存池
    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);
    //初始化緩存池,緩存池的可緩存數比緩存池的r總容量要小1
    setBucketsAndMask(newBuckets, newCapacity - 1);
    //釋放老的緩存池
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

結論:

開辟方法不僅僅是在新建容量池時候用,而且擴容時候也用這個方法,最后一句就是在建立新的容量池后,蘋果會將老的容量池釋放掉,這個需要注意

初始化緩存池方法:


void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.

    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    _buckets = newBuckets;
    
    // ensure other threads see new buckets before new mask
    mega_barrier();
    
    _mask = newMask;
    _occupied = 0;
}

作者:海浪寶寶
鏈接:https://juejin.cn/post/6844904030804639752
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容