YYCache 源碼學(xué)習(xí)(一):YYMemoryCache

其實最近是在重新熟練Swift的使用,我想出了一個比較實用的方法,那就是一邊看OC的項目,看懂之后用Swift實現(xiàn)一遍。這樣既學(xué)習(xí)了優(yōu)秀的源碼又練習(xí)了Swift,一舉兩得。

之前看過幾篇文章是剖析YYKit里面的一些小模塊,對源碼對一些解讀。不得不說作者ibireme的設(shè)計思維和技術(shù)細(xì)節(jié)的處理都非常的棒。所以就選了YYKit里面的一些小模塊入手。

YYCache主要分為了兩部分:YYMemoryCache內(nèi)存緩存和磁盤緩存YYDiskCache。平常使用的時候我們一般都只直接操作YYCache這個類,他是對內(nèi)存緩存和磁盤緩存的封裝。

這篇文章主要是講解YYCache模塊里面的YYMemoryCache部分。

API

我們先可以看一下YYMemoryCache的.h文件,瀏覽一起屬性和方法。大多數(shù)的都可以見名知意的。

@interface YYMemoryCache : NSObject

#pragma mark - Attribute
///=============================================================================
/// @name Attribute
///=============================================================================

@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;

#pragma mark - Limit
///=============================================================================
/// @name Limit
///=============================================================================

@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit; //過期時間
@property NSTimeInterval autoTrimInterval;//自動處理的間隔時間
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
@property BOOL releaseOnMainThread;
@property BOOL releaseAsynchronously;

#pragma mark - Access Methods
///=============================================================================
/// @name Access Methods
///=============================================================================
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;

#pragma mark - Trim
///=============================================================================
/// @name Trim
///=============================================================================

- (void)trimToCount:(NSUInteger)count;

- (void)trimToCost:(NSUInteger)cost;

- (void)trimToAge:(NSTimeInterval)age;

@end

我把亂七八糟的注釋都刪掉了,這樣可以直觀的來看,api分為四個部分,前兩部分都是一些屬性,后面兩個是方法。

第一個Attribute部分是YYMemoryCache類儲存的一些基本的屬性:name,totalCount(儲存對象的總個數(shù)),totalCost(儲存的總占內(nèi)存)。Limit部分是一些限制條件,就不一一的說了,單說一個releaseOnMainThread這個屬性,可能會有因為,如果如果能異步釋放,為什么還要強(qiáng)制去主線程釋放呢 ? 這是因為有一些類,像UIView/CALayer這種是要在主線程中釋放的,源碼注釋中也有提到。

第三部分就是一些跟儲存相關(guān)的方法,最后一部分就是根據(jù)限制條件修剪處理內(nèi)存的方法了 ~

.m代碼剖析

LRU 緩存淘汰算法

YYMemoryCache是提供了內(nèi)存修剪的方法的,既然有修剪,那么我們得有一個算法來確定是修剪掉哪一些。YYMemoryCache 和 YYDiskCache 都是實現(xiàn)的 LRU (least-recently-used) ,即最近最少使用淘汰算法。具體怎么樣實現(xiàn)我們往后再說。

實現(xiàn)緩存方式

.m的最前面是實現(xiàn)了兩個內(nèi)部類_YYLinkedMap和_YYLinkedMapNode。 可以看出具體的緩存方法是通過一個雙向列表和散列容器來實現(xiàn)的。

_YYLinkedMap中給出來操作結(jié)點的方法

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
具體細(xì)節(jié)剖析

先總起來說一下這些實現(xiàn)的代碼,其實很容易讀懂,就是通過一個鏈表的形式來處理緩存的數(shù)據(jù),添加緩存的時候,就往鏈表的尾部添加一個節(jié)點,(通過節(jié)點來表示我們實際要儲存的數(shù)據(jù)),如果要根據(jù)限制條件修剪內(nèi)存的話,也是循環(huán)的刪除尾部的那個節(jié)點,直到符合限制條件。那我們的LRU 緩存淘汰算法具體怎么使用,我發(fā)現(xiàn)在每一個讀取了一個數(shù)據(jù)之后,會把這個數(shù)據(jù)在鏈表中對應(yīng)的結(jié)點移動到頭部,這樣在大概率的情況下使用頻率高的緩存數(shù)據(jù)會在鏈表的前面。所以修剪的時候可以從尾部修剪。

在具體的實現(xiàn)代碼中,作者有很多很亮眼的操作,我們來欣賞一下。

1.定時修剪內(nèi)存

// 根據(jù)限制條件修剪內(nèi)存的占用  并根絕設(shè)定的時間遞歸調(diào)用
- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    //注意這個dispatch_after后面使用的dispatch_get_global_queue  異步
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        [self _trimInBackground];
        [self _trimRecursively];
    });
}

這個就是通過一個延時來調(diào)用修剪內(nèi)存的方法,然后在遞歸調(diào)用本身。注意的是dispatch_after后面使用的dispatch_get_global_queue 來進(jìn)行異步操作。

2.修剪內(nèi)存的邏輯

- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCost > costLimit) {
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        } else {
            usleep(10 * 1000); //10 ms
        }
    }
    //釋放
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

我們以這個按照占內(nèi)存大小的限制來修剪內(nèi)存為例子來看一下具體的實現(xiàn)方法。

開始先做幾個判斷,看是否超過限制需要修剪,不需要修剪就直接return。

修剪的過程就跟上面說到的是一樣的,判斷是否超過內(nèi)存限制,超過就刪掉尾部的結(jié)點,如此循環(huán)操作,只要符合限制要求。

異步釋放資源:

我們可以看到在removeNode的時候,會使用一個holder數(shù)組來接收被移除的這些node,然后最后釋放這些結(jié)點。為什么要這樣做?其實這就是通過異步釋放這些資源來減少主線程資源的開銷。這里作者在異步中調(diào)用了[holder count]; 其實最開始我也不知道這個是什么意思,但是作者標(biāo)注了release in queue,我猜測是通過調(diào)用你這個holder的隨便一個方法,讓這個異步的線程來管理這個holder,進(jìn)而通過此異步線程來實現(xiàn)holder中對象的釋放

鎖的使用:

還有一個比較重要的點,為什么使用pthread_mutex_trylock這個方式加鎖,然后在失敗之后,線程要sleep。

這個問題就需要我們?nèi)パ芯恳幌赂鞣N鎖了。很慚愧我對鎖的了解不是很深刻,但是通過看了大神的博客,有了一些了解。(下面內(nèi)容引用自大神的博客,文末有地址)

作者都是使用的pthread_mutex_t互斥鎖,這個鎖有一個特性,在多個線程競爭一個資源的時候,除了競爭成功的線程,其他的線程都會被動掛起狀態(tài),當(dāng)競爭成功的線程解鎖是,會去主動將掛起的其他線程激活,這個過程包含了上下文切換,CPU搶占,信號發(fā)送等開銷,很明顯,開銷有些大。

所以作者使用了pthread_mutex_trylock()嘗試解鎖,若解鎖失敗該方法會立即返回,讓當(dāng)前線程不會進(jìn)入被動的掛起狀態(tài)(也可以說阻塞),在下一次循環(huán)時又繼續(xù)嘗試獲取鎖。這個過程很有意思,感覺是手動實現(xiàn)了一個自旋鎖。而自旋鎖有個需要注意的問題是:死循環(huán)等待的時間越長,對 cpu 的消耗越大。所以作者做了一個很短的睡眠 usleep(10 * 1000),有效的減小了循環(huán)的調(diào)用次數(shù),至于這個睡眠時間的長度為什么是 10ms, 作者應(yīng)該做了測試。

其他部分

其他部分就不一一細(xì)說了,作者整體思路很清晰,然后代碼邏輯也很好懂,像上面提到的一些細(xì)節(jié)的處理可見作者的技術(shù)水平了。

參考

http://www.itdecent.cn/p/408d4d37bcbd

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

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是我自己在秋招復(fù)習(xí)時的讀書筆記,整理的知識點,也是為了防止忘記,尊重勞動成果,轉(zhuǎn)載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,600評論 4 56
  • 從 YYCache 源碼 Get 到如何設(shè)計一個優(yōu)秀的緩存 來源:Lision 前言 iOS 開發(fā)中總會用到各種緩...
    今天lgw閱讀 6,259評論 1 22
  • 今天開始分析YYCache 包含的文件類 YYCache YYMemoryCache YYDiskCache YY...
    充滿活力的早晨閱讀 912評論 4 1
  • 選擇安裝方式 CD/USB Arch啟動盤安裝 使用Arch啟動盤比較簡單方便,沒有額外設(shè)置,直接閱讀下一步。US...
    孤逐王閱讀 2,873評論 2 5
  • redis 是一個開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫...
    djyuning閱讀 2,749評論 2 8

友情鏈接更多精彩內(nèi)容