YYCache 源碼解析

YYCache是國內開發者ibireme開源的一個線程安全的高性能緩存組件,代碼風格簡潔清晰,在GitHub上已經有了1600+顆星。前端

閱讀它的源碼有助於創建比較完整的緩存設計的思路,同時也能鞏固一下雙向鏈表,線程鎖,數據庫操做相關的知識。若是你尚未看過YYCache的源碼,那麼恭喜你,閱讀此文會對理解YYCache的源碼有比較大的幫助。node

在正式開始講解源碼以前,先簡單看一下該框架的使用方法。git

基本使用方法

舉一個緩存用戶姓名的例子來看一下YYCache的幾個API:程序員

//須要緩存的對象
    NSString *userName = @"Jack";
   
   //須要緩存的對象在緩存裏對應的鍵
    NSString *key = @"user_name";
    
    //建立一個YYCache實例:userInfoCache
    YYCache *userInfoCache = [YYCache cacheWithName:@"userInfo"];
    
    //存入鍵值對
    [userInfoCache setObject:userName forKey:key withBlock:^{
        NSLog(@"caching object succeed");
    }];
    
    //判斷緩存是否存在
    [userInfoCache containsObjectForKey:key withBlock:^(NSString * _Nonnull key, BOOL contains) {
        if (contains){
            NSLog(@"object exists");
        }
    }];

    //根據key讀取數據
    [userInfoCache objectForKey:key withBlock:^(NSString * _Nonnull key, id<NSCoding>  _Nonnull object) {
        NSLog(@"user name : %@",object);
    }];

    //根據key移除緩存
    [userInfoCache removeObjectForKey:key withBlock:^(NSString * _Nonnull key) {
        NSLog(@"remove user name %@",key);
    }];
    
    //移除全部緩存
    [userInfoCache removeAllObjectsWithBlock:^{
        NSLog(@"removing all cache succeed");
    }];

    //移除全部緩存帶進度
    [userInfoCache removeAllObjectsWithProgressBlock:^(int removedCount, int totalCount) {
        NSLog(@"remove all cache objects: removedCount :%d totalCount : %d",removedCount,totalCount);
    } endBlock:^(BOOL error) {
        if(!error){
            NSLog(@"remove all cache objects: succeed");
        }else{
            NSLog(@"remove all cache objects: failed");
        }
    }];
複製代碼

整體來看這些API與NSCache是差很少的。 再來看一下框架的架構圖與成員職責劃分。github

架構圖與成員職責劃分

架構圖

成員職責劃分

從架構圖上來看,該組件裏面的成員並很少:算法

  • YYCache:提供了最外層的接口,調用了YYMemoryCache與YYDiskCache的相關方法。
  • YYMemoryCache:負責處理容量小,相對高速的內存緩存。線程安全,支持自動和手動清理緩存等功能。
  • _YYLinkedMap:YYMemoryCache使用的雙向鏈表類。
  • _YYLinkedMapNode:是_YYLinkedMap使用的節點類。
  • YYDiskCache:負責處理容量大,相對低速的磁盤緩存。線程安全,支持異步操做,自動和手動清理緩存等功能。
  • YYKVStorage:YYDiskCache的底層實現類,用於管理磁盤緩存。
  • YYKVStorageItem:內置在YYKVStorage中,是YYKVStorage內部用於封裝某個緩存的類。

代碼講解

知道了YYCache的架構圖與成員職責劃分之後,如今結合代碼開始正式講解。 講解分爲下面6個部分:sql

  • YYCache
  • YYMemoryCache
  • YYDiskCache
  • 保證線程安全的不一樣方案
  • 提升緩存性能的幾個嘗試
  • 其餘知識點

YYCache

YYCache給用戶提供全部最外層的緩存操做接口,而這些接口的內部內部其實是調用了YYMemoryCache和YYDiskCache對象的相關方法。數據庫

咱們來看一下YYCache的屬性和接口:編程

YYCache的屬性和接口

@interface YYCache : NSObject


@property (copy, readonly) NSString *name;//緩存名稱
@property (strong, readonly) YYMemoryCache *memoryCache;//內存緩存
@property (strong, readonly) YYDiskCache *diskCache;//磁盤緩存

//是否包含某緩存,無回調
- (BOOL)containsObjectForKey:(NSString *)key;
//是否包含某緩存,有回調
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;

//獲取緩存對象,無回調
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
//獲取緩存對象,有回調
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;

//寫入緩存對象,無回調
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
//寫入緩存對象,有回調
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;

//移除某緩存,無回調
- (void)removeObjectForKey:(NSString *)key;
//移除某緩存,有回調
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;

//移除全部緩存,無回調
- (void)removeAllObjects;
//移除全部緩存,有回調
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
//移除全部緩存,有進度和完成的回調
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;

@end
複製代碼

從上面的接口能夠看出YYCache的接口和NSCache很相近,並且在接口上都區分了有無回調的功能。 下面結合代碼看一下這些接口是如何實現的:後端

YYCache的接口實現

下面省略了帶有回調的接口,由於與無回調的接口很是接近。

- (BOOL)containsObjectForKey:(NSString *)key {
    
    //先檢查內存緩存是否存在,再檢查磁盤緩存是否存在
    return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}

- (id<NSCoding>)objectForKey:(NSString *)key {
    
    //首先嚐試獲取內存緩存,而後獲取磁盤緩存
    id<NSCoding> object = [_memoryCache objectForKey:key];
    
    //若是內存緩存不存在,就會去磁盤緩存裏面找:若是找到了,則再次寫入內存緩存中;若是沒找到,就返回nil
    if (!object) {
        object = [_diskCache objectForKey:key];
        if (object) {
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}


- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    //先寫入內存緩存,後寫入磁盤緩存
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}


- (void)removeObjectForKey:(NSString *)key {
    //先移除內存緩存,後移除磁盤緩存
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

- (void)removeAllObjects {
    //先所有移除內存緩存,後所有移除磁盤緩存
    [_memoryCache removeAllObjects];
    [_diskCache removeAllObjects];
}

複製代碼

從上面的接口實現能夠看出:在YYCache中,永遠都是先訪問內存緩存,而後再訪問磁盤緩存(包括了寫入,讀取,查詢,刪除緩存的操做)。並且關於內存緩存(_memoryCache)的操做,是不存在block回調的。

值得一提的是:在讀取緩存的操做中,若是在內存緩存中沒法獲取對應的緩存,則會去磁盤緩存中尋找。若是在磁盤緩存中找到了對應的緩存,則會將該對象再次寫入內存緩存中,保證在下一次嘗試獲取同一緩存時可以在內存中就能返回,提升速度

OK,如今瞭解了YYCache的接口以及實現,下面我分別講解一下YYMemoryCache(內存緩存)和YYDiskCache(磁盤緩存)這兩個類。

YYMemoryCache

YYMemoryCache負責處理容量小,相對高速的內存緩存:它將須要緩存的對象與傳入的key關聯起來,操做相似於NSCache。

可是與NSCache不一樣的是,YYMemoryCache的內部有:

  • 緩存淘汰算法:使用LRU(least-recently-used) 算法來淘汰(清理)使用頻率較低的緩存。
  • 緩存清理策略:使用三個維度來標記,分別是count(緩存數量),cost(開銷),age(距上一次的訪問時間)。YYMemoryCache提供了分別針對這三個維度的清理緩存的接口。用戶能夠根據不一樣的需求(策略)來清理在某一維度超標的緩存。

一個是淘汰算法,另外一個是清理維度,乍一看可能沒什麼太大區別。我在這裏先簡單區分一下:

緩存淘汰算法的目的在於區分出使用頻率高和使用頻率低的緩存,當緩存數量達到必定限制的時候會優先清理那些使用頻率低的緩存。由於使用頻率已經比較低的緩存在未來的使用頻率也頗有可能會低

緩存清理維度是給每一個緩存添加的標記:

  • 若是用戶須要刪除age(距上一次的訪問時間)超過1天的緩存,在YYMemoryCache內部,就會從使用頻率最低的那個緩存開始查找,直到全部距上一次的訪問時間超過1天的緩存都清理掉爲止。

  • 若是用戶須要將緩存總開銷清理到總開銷小於或等於某個值,在YYMemoryCache內部,就會從使用頻率最低的那個緩存開始清理,直到總開銷小於或等於這個值。

  • 若是用戶須要將緩存總數清理到總開銷小於或等於某個值,在YYMemoryCache內部,就會從使用頻率最低的那個緩存開始清理,直到總開銷小於或等於這個值。

能夠看出,不管是以哪一個維度來清理緩存,都是從緩存使用頻率最低的那個緩存開始清理。而YYMemoryCache保留的全部緩存的使用頻率的高低,是由LRU這個算法決定的。

如今知道了這兩者的區別,下面來具體講解一下緩存淘汰算法和緩存清理策略:

YYMemoryCache的緩存淘汰算法

在詳細講解這個算法以前我以爲有必要先說一下該算法的核心:

我我的認爲LRU緩存替換策略的核心在於若是某個緩存訪問的頻率越高,就認定用戶在未來越有可能訪問這個緩存。 因此在這個算法中,將那些最新訪問(寫入),最屢次被訪問的緩存移到最前面,而後那些很早以前寫入,不常常訪問的緩存就被自動放在了後面。這樣一來,在保留的緩存個數必定的狀況下,留下的緩存都是訪問頻率比較高的,這樣一來也就提高了緩存的命中率。誰都不想留着一些很難被用戶再次訪問的緩存,畢竟緩存自己也佔有必定的資源不是麼?

其實這個道理和一些商城類app的商品推薦邏輯是同樣的: 若是首頁只能展現10個商品,對於一個程序員用戶來講,可能推薦的是於那些他最近購買商品相似的機械鍵盤鼠標,技術書籍或者顯示屏之類的商品,而不是一些洋娃娃或是鋼筆之類的商品。

那麼LRU算法具體是怎麼作的呢?

在YYMemoryCache中,使用了雙向鏈表這個數據結構來保存這些緩存:

  • 當寫入一個新的緩存時,要把這個緩存節點放在鏈表頭部,而且而且原鏈表頭部的緩存節點要變成如今鏈表的第二個緩存節點。
  • 當訪問一個已有的緩存時,要把這個緩存節點移動到鏈表頭部,原位置兩側的緩存要接上,而且原鏈表頭部的緩存節點要變成如今鏈表的第二個緩存節點。
  • (根據清理維度)自動清理緩存時,要從鏈表的最後端逐個清理。

這樣一來,就能夠保證鏈表前端的緩存是最近寫入過和常常訪問過的。並且該算法老是從鏈表的最後端刪除緩存,這也就保證了留下的都是一些「比較新鮮的」緩存。

下面結合代碼來說解一下這個算法的實現:

YYMemoryCache用一個鏈表節點類來保存某個單獨的內存緩存的信息(鍵,值,緩存時間等),而後用一個雙向鏈表類來保存和管理這些節點。這兩個類的名稱分別是:

  • _YYLinkedMapNode:鏈表內的節點類,能夠看作是對某個單獨內存緩存的封裝。
  • _YYLinkedMap:雙向鏈表類,用於保存和管理全部內存緩存(節點)

_YYLinkedMapNode

_YYLinkedMapNode能夠被看作是對某個緩存的封裝:它包含了該節點上一個和下一個節點的指針,以及緩存的key和對應的值(對象),還有該緩存的開銷和訪問時間。

@interface _YYLinkedMapNode : NSObject {
    
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;              		  //緩存key
    id _value;              	          //key對應值
    NSUInteger _cost;                     //緩存開銷
    NSTimeInterval _time;                 //訪問時間
    
}
@end

@implementation _YYLinkedMapNode
@end
複製代碼

下面看一下雙向鏈表類:

_YYLinkedMap

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; 	// 用於存放節點
    NSUInteger _totalCost;   		//總開銷
    NSUInteger _totalCount;  		//節點總數
    _YYLinkedMapNode *_head;            // 鏈表的頭部結點
    _YYLinkedMapNode *_tail; 		// 鏈表的尾部節點
    BOOL _releaseOnMainThread; 	        //是否在主線程釋放,默認爲NO
    BOOL _releaseAsynchronously; 	//是否在子線程釋放,默認爲YES
}

//在鏈表頭部插入某節點
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//將鏈表內部的某個節點移到鏈表頭部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//移除某個節點
- (void)removeNode:(_YYLinkedMapNode *)node;

//移除鏈表的尾部節點並返回它
- (_YYLinkedMapNode *)removeTailNode;

//移除全部節點(默認在子線程操做)
- (void)removeAll;

@end
複製代碼

從鏈表類的屬性上看:鏈表類內置了CFMutableDictionaryRef,用於保存節點的鍵值對,它還持有了鏈表內節點的總開銷,總數量,頭尾節點等數據。

能夠參考下面這張圖來看一下兩者的關係:

看一下_YYLinkedMap的接口的實現:

將節點插入到鏈表頭部:

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    
    //設置該node的值
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    
    //增長開銷和總緩存數量
    _totalCost += node->_cost;
    _totalCount++;
    
    if (_head) {
        
        //若是鏈表內已經存在頭節點,則將這個頭節點賦給當前節點的尾指針(原第一個節點變成了現第二個節點)
        node->_next = _head;
        
        //將該節點賦給現第二個節點的頭指針(此時_head指向的節點是先第二個節點)
        _head->_prev = node;
        
        //將該節點賦給鏈表的頭結點指針(該節點變成了現第一個節點)
        _head = node;
        
    } else {
        
        //若是鏈表內沒有頭結點,說明是空鏈表。說明是第一次插入,則將鏈表的頭尾節點都設置爲當前節點
        _head = _tail = node;
    }
}
複製代碼

要看懂節點操做的代碼只要瞭解雙向鏈表的特性便可。在雙向鏈表中:

  • 每一個節點都有兩個分別指向先後節點的指針。因此說每一個節點都知道它前一個節點和後一個節點是誰。
  • 鏈表的頭部節點指向它前面節點的指針爲空;鏈表尾部節點指向它後側節點的指針也爲空。

爲了便於理解,咱們能夠把這個抽象概念類比於幼兒園手拉手的小朋友們: 每一個小朋友的左手都拉着前面小朋友的右手;每一個小朋友的右手都拉着後面小朋友的左手; 並且最前面的小朋友的左手和最後面的小朋友的右手都沒有拉任何一個小朋友。

將某個節點移動到鏈表頭部:

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    
    //若是該節點已是鏈表頭部節點,則當即返回,不作任何操做
    if (_head == node) return;
    
    
    if (_tail == node) {
        
        //若是該節點是鏈表的尾部節點
        //1. 將該節點的頭指針指向的節點變成鏈表的尾節點(將倒數第二個節點變成倒數第一個節點,即尾部節點)
        _tail = node->_prev;
        
        //2. 將新的尾部節點的尾部指針置空
        _tail->_next = nil;
        
    } else {
        
        //若是該節點是鏈表頭部和尾部之外的節點(中間節點)
        //1. 將該node的頭指針指向的節點賦給其尾指針指向的節點的頭指針
        node->_next->_prev = node->_prev;
        
        //2. 將該node的尾指針指向的節點賦給其頭指針指向的節點的尾指針
        node->_prev->_next = node->_next;
    }
    
    //將原頭節點賦給該節點的尾指針(原第一個節點變成了現第二個節點)
    node->_next = _head;
    
    //將當前節點的頭節點置空
    node->_prev = nil;
    
    //將現第二個節點的頭結點指向當前節點(此時_head指向的節點是現第二個節點)
    _head->_prev = node;
    
    //將該節點設置爲鏈表的頭節點
    _head = node;
}
複製代碼

第一次看上面的代碼我本身是懵逼的,不過若是結合上面小朋友拉手的例子就能夠快一點理解。 若是要其中一個小朋友放在隊伍的最前面,須要

  • 將原來這個小朋友先後的小朋友的手拉上。
  • 而後將這個小朋友的右手和原來排在第一位的小朋友的左手拉上。

上面說的比較簡略,可是相信對你們理解整個過程會有幫助。

也能夠再結合鏈表的圖解來看一下:

讀者一樣能夠利用這種思考方式理解下面這段代碼:

移除鏈表中的某個節點:

- (void)removeNode:(_YYLinkedMapNode *)node {
    
    //除去該node的鍵對應的值
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
    
    //減去開銷和總緩存數量
    _totalCost -= node->_cost;
    _totalCount--;
    
    //節點操做
    //1. 將該node的頭指針指向的節點賦給其尾指針指向的節點的頭指針
    if (node->_next) node->_next->_prev = node->_prev;
    
    //2. 將該node的尾指針指向的節點賦給其頭指針指向的節點的尾指針
    if (node->_prev) node->_prev->_next = node->_next;
    
    //3. 若是該node就是鏈表的頭結點,則將該node的尾部指針指向的節點賦給鏈表的頭節點(第二變成了第一)
    if (_head == node) _head = node->_next;
    
    //4. 若是該node就是鏈表的尾節點,則將該node的頭部指針指向的節點賦給鏈表的尾節點(倒數第二變成了倒數第一)
    if (_tail == node) _tail = node->_prev;
}
複製代碼

移除並返回尾部的node:

- (_YYLinkedMapNode *)removeTailNode {
    
    //若是不存在尾節點,則返回nil
    if (!_tail) return nil;
    
    _YYLinkedMapNode *tail = _tail;
    
    //移除尾部節點對應的值
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
    
    //減小開銷和總緩存數量
    _totalCost -= _tail->_cost;
    _totalCount--;
    
    if (_head == _tail) {
        //若是鏈表的頭尾節點相同,說明鏈表只有一個節點。將其置空
        _head = _tail = nil;
        
    } else {
        
        //將鏈表的尾節指針指向的指針賦給鏈表的尾指針(倒數第二變成了倒數第一)
        _tail = _tail->_prev;
        //將新的尾節點的尾指針置空
        _tail->_next = nil;
    }
    return tail;
}
複製代碼

OK,如今瞭解了YYMemoryCache底層的節點操做的代碼。如今來看一下YYMemoryCache是如何使用它們的。

YYMemoryCache的屬性和接口

//YYMemoryCache.h
@interface YYMemoryCache : NSObject

#pragma mark - Attribute

//緩存名稱,默認爲nil
@property (nullable, copy) NSString *name;

//緩存總數量
@property (readonly) NSUInteger totalCount;

//緩存總開銷
@property (readonly) NSUInteger totalCost;


#pragma mark - Limit

//數量上限,默認爲NSUIntegerMax,也就是無上限
@property NSUInteger countLimit;

//開銷上限,默認爲NSUIntegerMax,也就是無上限
@property NSUInteger costLimit;

//緩存時間上限,默認爲DBL_MAX,也就是無上限
@property NSTimeInterval ageLimit;

//清理超出上限以外的緩存的操做間隔時間,默認爲5s
@property NSTimeInterval autoTrimInterval;

//收到內存警告時是否清理全部緩存,默認爲YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;

//app進入後臺是是否清理全部緩存,默認爲YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;

//收到內存警告的回調block
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);

//進入後臺的回調block
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);

//緩存清理是否在後臺進行,默認爲NO
@property BOOL releaseOnMainThread;

//緩存清理是否異步執行,默認爲YES
@property BOOL releaseAsynchronously;


#pragma mark - 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

// =========== 緩存清理接口 =========== 
//清理緩存到指定個數
- (void)trimToCount:(NSUInteger)count;

//清理緩存到指定開銷
- (void)trimToCost:(NSUInteger)cost;

//清理緩存時間小於指定時間的緩存
- (void)trimToAge:(NSTimeInterval)age;
複製代碼

YYMemoryCache的接口實現

在YYMemoryCache的初始化方法裏,會實例化一個_YYLinkedMap的實例來賦給_lru這個成員變量。

- (instancetype)init{
      ....
      _lru = [_YYLinkedMap new];
      ...
  
  }
  
複製代碼

而後全部的關於緩存的操做,都要用到_lru這個成員變量,由於它纔是在底層持有這些緩存(節點)的雙向鏈表類。下面咱們來看一下這些緩存操做接口的實現:

//是否包含某個緩存對象
- (BOOL)containsObjectForKey:(id)key {

  //嘗試從內置的字典中得到緩存對象
  if (!key) return NO;
  pthread_mutex_lock(&_lock);
  BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
  pthread_mutex_unlock(&_lock);
  return contains;
}

//獲取某個緩存對象
- (id)objectForKey:(id)key {
  
  if (!key) return nil;
  
  pthread_mutex_lock(&_lock);
  _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
  if (node) {
      //若是節點存在,則更新它的時間信息(最後一次訪問的時間)
      node->_time = CACurrentMediaTime();
      [_lru bringNodeToHead:node];
  }
  pthread_mutex_unlock(&_lock);
  
  return node ? node->_value : nil;
}

//寫入某個緩存對象,開銷默認爲0
- (void)setObject:(id)object forKey:(id)key {
  [self setObject:object forKey:key withCost:0];
}


//寫入某個緩存對象,並存入緩存開銷
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
  
  if (!key) return;
  
  if (!object) {
      [self removeObjectForKey:key];
      return;
  }
  
  pthread_mutex_lock(&_lock);
  
  _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
  NSTimeInterval now = CACurrentMediaTime();
  
  if (node) {
      //若是存在與傳入的key值匹配的node,則更新該node的value,cost,time,並將這個node移到鏈表頭部
      
      //更新總cost
      _lru->_totalCost -= node->_cost;
      _lru->_totalCost += cost;
      
      //更新node
      node->_cost = cost;
      node->_time = now;
      node->_value = object;
      
      //將node移動至鏈表頭部
      [_lru bringNodeToHead:node];
      
  } else {
      
      //若是不存在與傳入的key值匹配的node,則新建一個node,將key,value,cost,time賦給它,並將這個node插入到鏈表頭部
      //新建node,並賦值
      node = [_YYLinkedMapNode new];
      node->_cost = cost;
      node->_time = now;
      node->_key = key;
      node->_value = object;
      
      //將node插入至鏈表頭部
      [_lru insertNodeAtHead:node];
  }
  
  //若是cost超過了限制,則進行刪除緩存操做(從鏈表尾部開始刪除,直到符合限制要求)
  if (_lru->_totalCost > _costLimit) {
      dispatch_async(_queue, ^{
          [self trimToCost:_costLimit];
      });
  }
  
  //若是total count超過了限制,則進行刪除緩存操做(從鏈表尾部開始刪除,刪除一次便可)
  if (_lru->_totalCount > _countLimit) {
      _YYLinkedMapNode *node = [_lru removeTailNode];
      if (_lru->_releaseAsynchronously) {
          dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
          dispatch_async(queue, ^{
              [node class]; //hold and release in queue
          });
      } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
          dispatch_async(dispatch_get_main_queue(), ^{
              [node class]; //hold and release in queue
          });
      }
  }
  pthread_mutex_unlock(&_lock);
}

//移除某個緩存對象
- (void)removeObjectForKey:(id)key {
  
  if (!key) return;
  
  pthread_mutex_lock(&_lock);
  _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
  if (node) {
  
      //內部調用了鏈表的removeNode:方法
      [_lru removeNode:node];
      if (_lru->_releaseAsynchronously) {
          dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
          dispatch_async(queue, ^{
              [node class]; //hold and release in queue
          });
      } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
          dispatch_async(dispatch_get_main_queue(), ^{
              [node class]; //hold and release in queue
          });
      }
  }
  pthread_mutex_unlock(&_lock);
}


//內部調用了鏈表的removeAll方法
- (void)removeAllObjects {
  pthread_mutex_lock(&_lock);
  [_lru removeAll];
  pthread_mutex_unlock(&_lock);
}
複製代碼

上面的實現是針對緩存的查詢,寫入,獲取操做的,接下來看一下緩存的清理策略。

YYMemoryCache的緩存清理策略

如上文所說,在YYCache中,緩存的清理能夠從緩存總數量,緩存總開銷,緩存距上一次的訪問時間來清理緩存。並且每種維度的清理操做均可以分爲自動和手動的方式來進行。

緩存自動清理

緩存的自動清理功能在YYMemoryCache初始化以後就開始了,是一個遞歸調用的實現:

//YYMemoryCache.m
- (instancetype)init{
    
    ...
    
    //開始按期清理
    [self _trimRecursively];
    
    ...
}


//遞歸清理,相隔時間爲_autoTrimInterval,在初始化以後當即執行
- (void)_trimRecursively {
    
    __weak typeof(self) _self = self;
    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];
            
    });
}

//清理全部不符合限制的緩存,順序爲:cost,count,age
- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
        
    });
}

複製代碼
//YYMemoryCache.m
- (void)trimToCount:(NSUInteger)count {
    if (count == 0) {
        [self removeAllObjects];
        return;
    }
    [self _trimToCount:count];
}

- (void)trimToCost:(NSUInteger)cost {
    [self _trimToCost:cost];
}

- (void)trimToAge:(NSTimeInterval)age {
    [self _trimToAge:age];
}
複製代碼

能夠看到,YYMemoryCache是按照緩存數量,緩存開銷,緩存時間的順序來自動清空緩存的。咱們結合代碼看一下它是如何按照緩存數量來清理緩存的(其餘兩種清理方式相似,暫不給出):

//YYMemoryCache.m

//將內存緩存數量降至等於或小於傳入的數量;若是傳入的值爲0,則刪除所有內存緩存
- (void)_trimToCount:(NSUInteger)countLimit {
    
    BOOL finish = NO;
    
    pthread_mutex_lock(&_lock);
    
    //若是傳入的參數=0,則刪除全部內存緩存
    if (countLimit == 0) {
        
        [_lru removeAll];
        finish = YES;
        
    } else if (_lru->_totalCount <= countLimit) {
    
        //若是當前緩存的總數量已經小於或等於傳入的數量,則直接返回YES,不進行清理
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        
        //==0的時候說明在嘗試加鎖的時候,獲取鎖成功,從而能夠進行操做;不然等待10秒(可是不知道爲何是10s而不是2s,5s,等等)
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCount > countLimit) {
                _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
        });
    }
}
複製代碼

緩存手動清理

其實上面這三種清理的方法在YYMemoryCache封裝成了接口,因此用戶也能夠經過YYCache的memoryCache這個屬性來手動清理相應維度上不符合傳入標準的緩存:

//YYMemoryCache.h

// =========== 緩存清理接口 =========== 
//清理緩存到指定個數
- (void)trimToCount:(NSUInteger)count;

//清理緩存到指定開銷
- (void)trimToCost:(NSUInteger)cost;

//清理緩存時間小於指定時間的緩存
- (void)trimToAge:(NSTimeInterval)age;
複製代碼

看一下它們的實現:

//清理緩存到指定個數
- (void)trimToCount:(NSUInteger)count {
    if (count == 0) {
        [self removeAllObjects];
        return;
    }
    [self _trimToCount:count];
}

//清理緩存到指定開銷
- (void)trimToCost:(NSUInteger)cost {
    [self _trimToCost:cost];
}

//清理緩存時間小於指定時間的緩存
- (void)trimToAge:(NSTimeInterval)age {
    [self _trimToAge:age];
}
複製代碼

YYDiskCache

YYDiskCache負責處理容量大,相對低速的磁盤緩存。線程安全,支持異步操做。做爲YYCache的第二級緩存,它與第一級緩存YYMemoryCache的相同點是:

  • 都具備查詢,寫入,讀取,刪除緩存的接口。
  • 不直接操做緩存,也是間接地經過另外一個類(YYKVStorage)來操做緩存。
  • 它使用LRU算法來清理緩存。
  • 支持按 cost,count 和 age 這三個維度來清理不符合標準的緩存。

它與YYMemoryCache不一樣點是:

    1. 根據緩存數據的大小來採起不一樣的形式的緩存:
    • 數據庫sqlite: 針對小容量緩存,緩存的data和元數據都保存在數據庫裏。
    • 文件+數據庫的形式: 針對大容量緩存,緩存的data寫在文件系統裏,其元數據保存在數據庫裏。
    1. 除了 cost,count 和 age 三個維度以外,還添加了一個磁盤容量的維度。

這裏須要說明的是: 對於上面的第一條:我看源碼的時候只看出來有這兩種緩存形式,可是從內部的緩存type枚舉來看,實際上是分爲三種的:

typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    
    YYKVStorageTypeFile = 0,
    YYKVStorageTypeSQLite = 1,
    YYKVStorageTypeMixed = 2,
};
複製代碼

也就是說我只找到了第二,第三種緩存形式,而第一種純粹的文件存儲(YYKVStorageTypeFile)形式的實現我沒有找到:當type爲 YYKVStorageTypeFile和YYKVStorageTypeMixed的時候的緩存實現都是一致的:都是講data存在文件裏,將元數據放在數據庫裏面。

在YYDiskCache的初始化方法裏,沒有發現正確的將緩存類型設置爲YYKVStorageTypeFile的方法:

//YYDiskCache.m

- (instancetype)init {
    @throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
    return [self initWithPath:@"" inlineThreshold:0];
}

- (instancetype)initWithPath:(NSString *)path {
    return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {

   ...    
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    
   ...
}

複製代碼

從上面的代碼能夠看出來,當給指定初始化方法initWithPath:inlineThreshold:的第二個參數傳入0的時候,緩存類型纔是YYKVStorageTypeFile。並且比較經常使用的初始化方法initWithPath:的實現裏,是將20kb傳入了指定初始化方法裏,結果就是將type設置成了YYKVStorageTypeMixed。

並且我也想不出若是隻有文件形式的緩存的話,其元數據如何保存。若是有讀者知道的話,麻煩告知一下,很是感謝了~~

在本文暫時對於上面提到的」文件+數據庫的形式」在下文統一說成文件緩存了。

在接口的設計上,YYDiskCache與YYMemoryCache是高度一致的,只不過由於有些時候大文件的訪問可能會比較耗時,因此框架做者在保留了與YYMemoryCache同樣的接口的基礎上,還在原來的基礎上添加了block回調,避免阻塞線程。來看一下YYDiskCache的接口(省略了註釋):

//YYDiskCache.h

- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block;


- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;


- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(void(^)(void))block;


- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block;


- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;


- (NSInteger)totalCount;
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block;


- (NSInteger)totalCost;
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block;


#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block;


- (void)trimToCost:(NSUInteger)cost;
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block;

- (void)trimToAge:(NSTimeInterval)age;
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block;
複製代碼

從上面的接口代碼能夠看出,YYDiskCache與YYMemoryCache在接口設計上是很是類似的。可是,YYDiskCache有一個很是重要的屬性,它做爲用sqlite作緩存仍是用文件作緩存的分水嶺

//YYDiskCache.h
@property (readonly) NSUInteger inlineThreshold;
複製代碼

這個屬性的默認值是20480byte,也就是20kb。便是說,若是緩存數據的長度大於這個值,就使用文件存儲;若是小於這個值,就是用sqlite存儲。來看一下這個屬性是如何使用的:

首先咱們會在YYDiskCache的指定初始化方法裏看到這個屬性:

//YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
   ...
    _inlineThreshold = threshold;
    ...
}
複製代碼

在這裏將_inlineThreshold賦值,也是惟一一次的賦值。而後在寫入緩存的操做裏判斷寫入緩存的大小是否大於這個臨界值,若是是,則使用文件緩存:

//YYDiskCache.m
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
   
   ...
    NSString *filename = nil;
    if (_kv.type != YYKVStorageTypeSQLite) {
        //若是長度大臨界值,則生成文件名稱,使得filename不爲nil
        if (value.length > _inlineThreshold) {
            filename = [self _filenameForKey:key];
        }
    }
    
    Lock();
    //在該方法內部判斷filename是否爲nil,若是是,則使用sqlite進行緩存;若是不是,則使用文件緩存
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}
複製代碼

如今咱們知道了YYDiskCache相對於YYMemoryCache最大的不一樣之處是緩存類型的不一樣。 細心的朋友會發現上面這個寫入緩存的方法(saveItemWithKey:value:filename:extendedData:)其實是屬於_kv的。這個_kv就是上面提到的YYKVStorage的實例,它在YYDiskCache的初始化方法裏被賦值:

//YYDiskCache.m
- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    ...
    
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    _kv = kv;
    ...
}
複製代碼

一樣地,再舉其餘兩個接口爲例,內部也是調用了_kv的方法:

- (BOOL)containsObjectForKey:(NSString *)key {
    if (!key) return NO;
    Lock();
    BOOL contains = [_kv itemExistsForKey:key];
    Unlock();
    return contains;
}

- (void)removeObjectForKey:(NSString *)key {
    if (!key) return;
    Lock();
    [_kv removeItemForKey:key];
    Unlock();
} 
複製代碼

因此是時候來看一下YYKVStorage的接口和實現了:

YYKVStorage

YYKVStorage實例負責保存和管理全部磁盤緩存。和YYMemoryCache裏面的_YYLinkedMap將緩存封裝成節點類_YYLinkedMapNode相似,YYKVStorage也將某個單獨的磁盤緩存封裝成了一個類,這個類就是YYKVStorageItem,它保存了某個緩存所對應的一些信息(key, value, 文件名,大小等等):

//YYKVStorageItem.h

@interface YYKVStorageItem : NSObject

@property (nonatomic, strong) NSString *key;                //鍵
@property (nonatomic, strong) NSData *value;                //值
@property (nullable, nonatomic, strong) NSString *filename; //文件名
@property (nonatomic) int size;                             //值的大小,單位是byte
@property (nonatomic) int modTime;                          //修改時間戳
@property (nonatomic) int accessTime;                       //最後訪問的時間戳
@property (nullable, nonatomic, strong) NSData *extendedData; //extended data

@end
複製代碼

既然在這裏將緩存封裝成了YYKVStorageItem實例,那麼做爲緩存的管理者,YYKVStorage就必然有操做YYKVStorageItem的接口了:

//YYKVStorage.h

//寫入某個item
- (BOOL)saveItem:(YYKVStorageItem *)item;

//寫入某個鍵值對,值爲NSData對象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;

//寫入某個鍵值對,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
                  value:(NSData *)value
               filename:(nullable NSString *)filename
           extendedData:(nullable NSData *)extendedData;

#pragma mark - Remove Items

//移除某個鍵的item
- (BOOL)removeItemForKey:(NSString *)key;

//移除多個鍵的item
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;

//移除大於參數size的item
- (BOOL)removeItemsLargerThanSize:(int)size;

//移除時間早於參數時間的item
- (BOOL)removeItemsEarlierThanTime:(int)time;

//移除item,使得緩存總容量小於參數size
- (BOOL)removeItemsToFitSize:(int)maxSize;

//移除item,使得緩存數量小於參數size
- (BOOL)removeItemsToFitCount:(int)maxCount;

//移除全部的item
- (BOOL)removeAllItems;

//移除全部的item,附帶進度與結束block
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                               endBlock:(nullable void(^)(BOOL error))end;


#pragma mark - Get Items
//讀取參數key對應的item
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;

//讀取參數key對應的data
- (nullable NSData *)getItemValueForKey:(NSString *)key;

//讀取參數數組對應的item數組
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;

//讀取參數數組對應的item字典
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;
複製代碼

你們最關心的應該是寫入緩存的接口是如何實現的,下面重點講一下寫入緩存的接口:

//寫入某個item
- (BOOL)saveItem:(YYKVStorageItem *)item;

//寫入某個鍵值對,值爲NSData對象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;

//寫入某個鍵值對,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
                  value:(NSData *)value
               filename:(nullable NSString *)filename
           extendedData:(nullable NSData *)extendedData;
複製代碼

這三個接口都比較相似,上面的兩個方法都會調用最下面參數最多的方法。在詳細講解寫入緩存的代碼以前,我先講一下寫入緩存的大體邏輯,有助於讓你們理解整個YYDiskCache寫入緩存的流程:

  1. 首先判斷傳入的key和value是否符合要求,若是不符合要求,則當即返回NO,緩存失敗。
  2. 再判斷是否type==YYKVStorageTypeFile而且文件名爲空字符串(或nil):若是是,則當即返回NO,緩存失敗。
  3. 判斷filename是否爲空字符串:
  4. 若是不爲空:寫入文件,並將緩存的key,等信息寫入數據庫,可是不將key對應的data寫入數據庫。
  5. 若是爲空:
  6. 若是緩存類型爲YYKVStorageTypeSQLite:將緩存文件刪除
  7. 若是緩存類型不爲YYKVStorageTypeSQLite:則將緩存的key和對應的data等其餘信息存入數據庫。
- (BOOL)saveItem:(YYKVStorageItem *)item {
    return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];
}

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {
    return [self saveItemWithKey:key value:value filename:nil extendedData:nil];
}

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    
    if (key.length == 0 || value.length == 0) return NO;
    
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    
    if (filename.length) {
        
        //若是文件名不爲空字符串,說明要進行文件緩存
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        
        //寫入元數據
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            //若是緩存信息保存失敗,則刪除對應的文件
            [self _fileDeleteWithName:filename];
            return NO;
        }
        
        return YES;
        
    } else {
        
        //若是文件名爲空字符串,說明不要進行文件緩存
        if (_type != YYKVStorageTypeSQLite) {
            
            //若是緩存類型不是數據庫緩存,則查找出相應的文件名並刪除
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        
        // 緩存類型是數據庫緩存,把元數據和value寫入數據庫
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}
複製代碼

從上面的代碼能夠看出,在底層寫入緩存的方法是_dbSaveWithKey:value:fileName:extendedData:,這個方法使用了兩次:

  • 在以文件(和數據庫)存儲緩存時
  • 在以數據庫存儲緩存時

不過雖然調用了兩次,咱們能夠從傳入的參數是有差異的:第二次filename傳了nil。那麼咱們來看一下_dbSaveWithKey:value:fileName:extendedData:內部是如何區分有無filename的狀況的:

  • 當filename爲空時,說明在外部沒有寫入該緩存的文件:則把data寫入數據庫裏
  • 當filename不爲空時,說明在外部有寫入該緩存的文件:則不把data也寫入了數據庫裏

下面結合代碼看一下:

//數據庫存儲
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    
    //sql語句
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
    
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    
    //key
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
    
    //filename
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
    
    //size
    sqlite3_bind_int(stmt, 3, (int)value.length);
    
    //inline_data
    if (fileName.length == 0) {
        
        //若是文件名長度==0,則將value存入數據庫
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
        
    } else {
        
        //若是文件名長度不爲0,則不將value存入數據庫
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    
    //modification_time
    sqlite3_bind_int(stmt, 5, timestamp);
    
    //last_access_time
    sqlite3_bind_int(stmt, 6, timestamp);
    
    //extended_data
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
    
    int result = sqlite3_step(stmt);
    
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    
    return YES;
}
複製代碼

框架做者用數據庫的一條記錄來保存關於某個緩存的全部信息。 並且數據庫的第四個字段是保存緩存對應的data的,從上面的代碼能夠看出當filename爲空和不爲空的時候的處理的差異。

上面的sqlite3_stmt能夠看做是一個已經把sql語句解析了的、用sqlite本身標記記錄的內部數據結構。 而sqlite3_bind_text和sqlite3_bind_int是綁定函數,能夠看做是將變量插入到字段的操做。

OK,如今看完了寫入緩存,咱們再來看一下獲取緩存的操做:

//YYKVSorage.m
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    
    if (key.length == 0) return nil;
    
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    
    if (item) {
        //更新內存訪問的時間
        [self _dbUpdateAccessTimeWithKey:key];
        
        if (item.filename) {
            //若是有文件名,則嘗試獲取文件數據
            item.value = [self _fileReadWithName:item.filename];
            //若是此時獲取文件數據失敗,則刪除對應的item
            if (!item.value) {
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}
複製代碼

從上面這段代碼咱們能夠看到獲取YYKVStorageItem的實例的方法是_dbGetItemWithKey:excludeInlineData: 咱們來看一下它的實現:

  1. 首先根據查找key的sql語句生成stmt
  2. 而後將傳入的key與該stmt進行綁定
  3. 最後經過這個stmt來查找出與該key對應的有關該緩存的其餘數據並生成item。

來看一下代碼:

- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
    NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    if (!stmt) return nil;
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
    
    YYKVStorageItem *item = nil;
    int result = sqlite3_step(stmt);
    if (result == SQLITE_ROW) {
        //傳入stmt來生成YYKVStorageItem實例
        item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData];
    } else {
        if (result != SQLITE_DONE) {
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        }
    }
    return item;
}
複製代碼

咱們能夠看到最終生成YYKVStorageItem實例的是經過_dbGetItemFromStmt:excludeInlineData:來實現的:

- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
    
    //提取數據
    int i = 0;
    char *key = (char *)sqlite3_column_text(stmt, i++);
    char *filename = (char *)sqlite3_column_text(stmt, i++);
    int size = sqlite3_column_int(stmt, i++);
    
    //判斷excludeInlineData
    const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i);
    int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++);
    
    
    int modification_time = sqlite3_column_int(stmt, i++);
    int last_access_time = sqlite3_column_int(stmt, i++);
    const void *extended_data = sqlite3_column_blob(stmt, i);
    int extended_data_bytes = sqlite3_column_bytes(stmt, i++);
    
    //將數據賦給item的屬性
    YYKVStorageItem *item = [YYKVStorageItem new];
    if (key) item.key = [NSString stringWithUTF8String:key];
    if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename];
    item.size = size;
    if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes];
    item.modTime = modification_time;
    item.accessTime = last_access_time;
    if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes];
    return item;
}
複製代碼

上面這段代碼分爲兩個部分:

  • 獲取數據庫裏每個字段對應的數據
  • 將數據賦給YYKVStorageItem的實例

須要注意的是:

  1. 字符串類型須要使用stringWithUTF8String:來轉成NSString類型。
  2. 這裏面會判斷excludeInlineData
  • 若是爲TRUE,就提取存入的data數據
  • 若是爲FALSE,就不提取

保證線程安全的方案

我相信對於某個設計來講,它的產生必定是基於某種個特定問題下的某個場景的

由上文能夠看出:

  • YYMemoryCache 使用了 pthread_mutex 線程鎖(互斥鎖)來確保線程安全
  • YYDiskCache 則選擇了更適合它的 dispatch_semaphore。

內存緩存操做的互斥鎖

在YYMemoryCache中,是使用互斥鎖來保證線程安全的。 首先在YYMemoryCache的初始化方法中獲得了互斥鎖,並在它的全部接口裏都加入了互斥鎖來保證線程安全,包括setter,getter方法和緩存操做的實現。舉幾個例子:

- (NSUInteger)totalCost {
    pthread_mutex_lock(&_lock);
    NSUInteger totalCost = _lru->_totalCost;
    pthread_mutex_unlock(&_lock);
    return totalCost;
}

- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {
    pthread_mutex_lock(&_lock);
    _lru->_releaseOnMainThread = releaseOnMainThread;
    pthread_mutex_unlock(&_lock);
}

- (BOOL)containsObjectForKey:(id)key {
    
    if (!key) return NO;
    pthread_mutex_lock(&_lock);
    BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
    pthread_mutex_unlock(&_lock);
    return contains;
}

- (id)objectForKey:(id)key {
    
    if (!key) return nil;
    
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
        //若是節點存在,則更新它的時間信息(最後一次訪問的時間)
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    
    return node ? node->_value : nil;
}

複製代碼

並且須要在dealloc方法中銷燬這個鎖頭:

- (void)dealloc {
    
    ...
    
    //銷燬互斥鎖
    pthread_mutex_destroy(&_lock);
}
複製代碼

磁盤緩存使用信號量來代替鎖

框架做者採用了信號量的方式來給 首先在初始化的時候實例化了一個信號量:

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    ...
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
    ...
複製代碼

而後使用了宏來代替加鎖解鎖的代碼:

#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
複製代碼

簡單說一下信號量:

dispatch_semaphore是GCD用來同步的一種方式,與他相關的共有三個函數,分別是

  • dispatch_semaphore_create:定義信號量
  • dispatch_semaphore_signal:使信號量+1
  • dispatch_semaphore_wait:使信號量-1

當信號量爲0時,就會作等待處理,這是其餘線程若是訪問的話就會讓其等待。因此若是信號量在最開始的的時候被設置爲1,那麼就能夠實現「鎖」的功能:

  • 執行某段代碼以前,執行dispatch_semaphore_wait函數,讓信號量-1變爲0,執行這段代碼。
  • 此時若是其餘線程過來訪問這段代碼,就要讓其等待。
  • 當這段代碼在當前線程結束之後,執行dispatch_semaphore_signal函數,令信號量再次+1,那麼若是有正在等待的線程就能夠訪問了。

須要注意的是:若是有多個線程等待,那麼後來信號量恢復之後訪問的順序就是線程遇到dispatch_semaphore_wait的順序。

這也就是信號量和互斥鎖的一個區別:互斥量用於線程的互斥,信號線用於線程的同步。

  • 互斥:是指某一資源同時只容許一個訪問者對其進行訪問,具備惟一性和排它性。但互斥沒法限制訪問者對資源的訪問順序,即訪問是無序的

  • 同步:是指在互斥的基礎上(大多數狀況),經過其它機制實現訪問者對資源的有序訪問。在大多數狀況下,同步已經實現了互斥,特別是全部寫入資源的狀況一定是互斥的。也就是說使用信號量可使多個線程有序訪問某個資源。

那麼問題來了:爲何內存緩存使用的是互斥鎖(pthread_mutex),而磁盤緩存使用的就是信號量(dispatch_semaphore)呢?

答案在框架做者的文章YYCache 設計思路裏能夠找到:

爲何內存緩存使用互斥鎖(pthread_mutex)?

框架做者在最初使用的是自旋鎖(OSSpinLock)做爲內存緩存的線程鎖,可是後來得知其不夠安全,因此退而求其次,使用了pthread_mutex。

爲何磁盤緩存使用的是信號量(dispatch_semaphore)?

dispatch_semaphore 是信號量,但當信號總量設爲 1 時也能夠看成鎖來。在沒有等待狀況出現時,它的性能比 pthread_mutex 還要高,但一旦有等待狀況出現時,性能就會降低許多。相對於 OSSpinLock 來講,它的優點在於等待時不會消耗 CPU 資源。對磁盤緩存來講,它比較合適。

由於YYDiskCache在寫入比較大的緩存時,可能會有比較長的等待時間,而dispatch_semaphore在這個時候是不消耗CPU資源的,因此比較適合。

提升緩存性能的幾個嘗試

選擇合適的線程鎖

能夠參考上一部分YYMemoryCache 和YYDiskCache使用的不一樣的鎖以及緣由。

選擇合適的數據結構

在YYMemoryCache中,做者選擇了雙向鏈表來保存這些緩存節點。那麼能夠思考一下,爲何要用雙向鏈表而不是單向鏈表或是數組呢?

  • 爲何不選擇單向鏈表:單鏈表的節點只知道它後面的節點(只有指向後一節點的指針),而不知道前面的。因此若是想移動其中一個節點的話,其先後的節點很差作銜接。

  • 爲何不選擇數組:數組中元素在內存的排列是連續的,對於尋址操做很是便利;可是對於插入,刪除操做很不方便,須要總體移動,移動的元素個數越多,代價越大。而鏈表偏偏相反,由於其節點的關聯僅僅是靠指針,因此對於插入和刪除操做會很便利,而尋址操做缺比較費時。因爲在LRU策略中會有很是多的移動,插入和刪除節點的操做,因此使用雙向鏈表是比較有優點的。

選擇合適的線程來操做不一樣的任務

不管緩存的自動清理和釋放,做者默認把這些任務放到子線程去作:

看一下釋放全部內存緩存的操做:

- (void)removeAll {
    
    //將開銷,緩存數量置爲0
    _totalCost = 0;
    _totalCount = 0;
    
    //將鏈表的頭尾節點置空
    _head = nil;
    _tail = nil;
    
    if (CFDictionaryGetCount(_dic) > 0) {
        
        CFMutableDictionaryRef holder = _dic;
        _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        
        //是否在子線程操做
        if (_releaseAsynchronously) {
            dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                CFRelease(holder); // hold and release in specified queue
            });
        } else if (_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                CFRelease(holder); // hold and release in specified queue
            });
        } else {
            CFRelease(holder);
        }
    }
}
複製代碼

這裏的YYMemoryCacheGetReleaseQueue()使用了內聯函數,返回了低優先級的併發隊列。

//內聯函數,返回優先級最低的全局併發隊列
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
複製代碼

選擇底層的類

一樣是字典實現,可是做者使用了更底層且快速的CFDictionary而沒有用NSDictionary來實現。

其餘知識點

禁用原生初始化方法並標明新定義的指定初始化方法

YYCache有4個供外部調用的初始化接口,不管是對象方法仍是類方法都須要傳入一個字符串(名稱或路徑)。

而兩個原生的初始化方法被框架做者禁掉了:

- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
複製代碼

若是用戶使用了上面兩個初始化方法就會在編譯期報錯。

而剩下的四個可使用的初始化方法中,有一個是指定初始化方法,被做者用NS_DESIGNATED_INITIALIZER標記了。

- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;

+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
複製代碼

指定初始化方法就是全部可以使用的初始化方法都必須調用的方法。更詳細的介紹能夠參考個人下面兩篇文章:

異步釋放對象的技巧

爲了異步將某個對象釋放掉,能夠經過在GCD的block裏面給它發個消息來實現。這個技巧在該框架中很常見,舉一個刪除一個內存緩存的例子:

首先將這個緩存的node類取出,而後異步將其釋放掉。

- (void)removeObjectForKey:(id)key {
    
    if (!key) return;
    
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
        [_lru removeNode:node];
        if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    pthread_mutex_unlock(&_lock);
}
複製代碼

爲了釋放掉這個node對象,在一個異步執行的(主隊列或自定義隊列裏)block裏給其發送了class這個消息。不須要糾結這個消息具體是什麼,他的目的是爲了不編譯錯誤,由於咱們沒法在block裏面硬生生地將某個對象寫進去。

其實關於上面這一點我本身也有點拿不許,但願理解得比較透徹的同窗能在下面留個言~ ^^

內存警告和進入後臺的監聽

YYCache默認在收到內存警告和進入後臺時,自動清除全部內存緩存。因此在YYMemoryCache的初始化方法裏,咱們能夠看到這兩個監聽的動做:

//YYMemoryCache.m

- (instancetype)init{
    
    ...
      
    //監聽app生命週期
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
   
    ...
}
複製代碼

而後實現監聽到消息後的處理方法:

//內存警告時,刪除全部內存緩存
- (void)_appDidReceiveMemoryWarningNotification {
    if (self.didReceiveMemoryWarningBlock) {
        self.didReceiveMemoryWarningBlock(self);
    }
    if (self.shouldRemoveAllObjectsOnMemoryWarning) {
        [self removeAllObjects];
    }
}


//進入後臺時,刪除全部內存緩存
- (void)_appDidEnterBackgroundNotification {
    if (self.didEnterBackgroundBlock) {
        self.didEnterBackgroundBlock(self);
    }
    if (self.shouldRemoveAllObjectsWhenEnteringBackground) {
        [self removeAllObjects];
    }
}

複製代碼

判斷頭文件的導入

#if __has_include(<YYCache/YYCache.h>)
#import <YYCache/YYMemoryCache.h>
#import <YYCache/YYDiskCache.h>
#import <YYCache/YYKVStorage.h>
#elif __has_include(<YYWebImage/YYCache.h>)
#import <YYWebImage/YYMemoryCache.h>
#import <YYWebImage/YYDiskCache.h>
#import <YYWebImage/YYKVStorage.h>
#else
#import "YYMemoryCache.h"
#import "YYDiskCache.h"
#import "YYKVStorage.h"
#endif
複製代碼

在這裏做者使用__has_include來檢查Frameworks是否引入某個類。 由於YYWebImage已經集成YYCache,因此若是導入過YYWebImage的話就無需重再導入YYCache了。

最後的話

經過看該組件的源碼,我收穫的不只有緩存設計的思路,還有:

  • 雙向鏈表的概念以及相關操做
  • 數據庫的使用
  • 互斥鎖,信號量的使用
  • 實現線程安全的方案
  • 變量,方法的命名以及接口的設計

相信讀過這篇文章的你也會有一些收穫~ 若是能趁熱打鐵,下載一個YYCache源碼看就更好啦~


本篇已同步到我的博客:傳送門

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者之前發佈的精選技術文章,以及後續發佈的技術文章(以原創爲主),而且逐漸脫離 iOS 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~

公衆號:程序員維他命
相關文章
相關標籤/搜索