YYCache 源碼剖析:一覽亮點

YYKit 系列源碼剖析文章:node

寫在前面

YYCache 做爲當下 iOS 圈最流行的緩存框架,有着優越的性能和絕佳的設計。筆者花了些時間對其「解剖」了一番,發現了不少有意思的東西,因此寫下本文分享一下。算法

考慮到篇幅,筆者對於源碼的解析不會過多的涉及 API 使用和一些基礎知識,更多的是剖析做者 ibireme 的設計思惟和重要技術實現細節。sql

YYCache 主要分爲兩部分:內存緩存和磁盤緩存(對應 YYMemoryCacheYYDiskCache)。在平常開發業務使用中,可能是直接操做 YYCache 類,該類是對內存緩存功能和磁盤緩存功能的一個簡單封裝。數據庫

源碼基於 1.0.4 版本。緩存

1、內存緩存:YYMemoryCache

總覽 API ,會發現一些見名知意的方法:安全

- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
......
複製代碼

能夠看出,該類主要包含讀寫功能和修剪功能(修剪是爲了控制內存緩存的大小等)。固然,還有其餘一些自定義方法,好比釋放操做的線程選擇、內存警告和進入後臺時是否清除內存緩存等。性能優化

對該類的基本功能有了瞭解以後,就能夠直接切實現源碼了。bash

(1)LRU 緩存淘汰算法

既然有修剪緩存的功能,必然涉及到一個緩存淘汰算法,YYMemoryCacheYYDiskCache 都是實現的 LRU (least-recently-used) ,即最近最少使用淘汰算法。數據結構

在 YYMemoryCache.m 文件中,有以下的代碼:多線程

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}
複製代碼

熟悉鏈表的朋友應該一眼就看出來貓膩,做者是使用的一個雙向鏈表+散列容器來實現 LRU 的。

鏈表的節點 (_YYLinkedMapNode):

  1. 同時使用前驅和後繼指針(即_prev_next)是爲了快速找到前驅和後繼節點。
  2. 這裏使用__unsafe_unretained而不使用__weak。雖然二者都不會持有指針所指向的對象,可是在指向對象釋放時,前者並不會自動置空指針,造成野指針,不過通過筆者後面的閱讀,發現做者避免了野指針的出現;並且從性能層面看(做者原話):訪問具備 __weak 屬性的變量時,實際上會調用objc_loadWeak()objc_storeWeak()來完成,這也會帶來很大的開銷,因此要避免使用__weak屬性。
  3. _key_value就是框架使用者想要存儲的鍵值對,能夠看出做者的設計是一個鍵值對對應一個節點(_YYLinkedMapNode)。
  4. _cost_time表示該節點的內存大小和最後訪問的時間。

LRU 實現類 (_YYLinkedMap) :

  1. 包含頭尾指針(_head_tail),保證雙端查詢的效率。
  2. _totalCost_totalCount記錄最大內存佔用限制和數量限制。
  3. _releaseOnMainThread_releaseAsynchronously分別表示在主線程釋放和在異步線程釋放,它們的實現後文會講到。
  4. _dic變量是 OC 開發中經常使用的散列容器,全部節點都會在_dic中以 key-value 的形式存在,保證常數級查詢效率。

既然是 LRU 算法,怎麼能只有數據結構,往下面看 _YYLinkedMap 類實現了以下算法(嗯,挺常規的節點操做,代碼質量挺高的,就不說明實現了):

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
複製代碼

如今 LRU 的數據結構和操做算法實現都有了,就能夠看具體的業務了。

##(2)修剪內存的邏輯

正如一開始貼的 API ,該類有三種修剪內存的依據:根據緩存的內存塊數量、根據佔用內存大小、根據是不是最近使用。它們的實現邏輯幾乎同樣,這裏就其中一個爲例子(代碼有刪減):

- (void)_trimToAge:(NSTimeInterval)ageLimit {
    ......
    
    NSMutableArray *holder = [NSMutableArray new];
//1 迭代部分
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) {
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        } else {
            usleep(10 * 1000); //10 ms
        }
    }
//2 釋放部分
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}
複製代碼

這裏有幾個重要技術點,頗有意思。

釋放尾節點

經過一個 while 循環不斷釋放尾節點removeTailNode,直到知足參數ageLimit對時間的要求,而該鏈表的排序規則是:最近使用的內存塊會移動到鏈表頭部,也就保證了刪除的內存永遠是最不常使用的(後面會看到如何實現排序的)。

鎖的處理

不妨思考這樣一個問題:爲什麼要使用pthread_mutex_trylock()方法嘗試獲取鎖,而獲取失敗事後作了一個線程掛起操做usleep()

**優先級反轉:**好比兩個線程 A 和 B,優先級 A < B。當 A 獲取鎖訪問共享資源時,B 嘗試獲取鎖,那麼 B 就會進入忙等狀態,忙等時間越長對 CPU 資源的佔用越大;而因爲 A 的優先級低於 B,A 沒法與高優先級的線程爭奪 CPU 資源,從而致使任務遲遲完成不了。解決優先級反轉的方法有「優先級天花板」和「優先級繼承」,它們的核心操做都是提高當前正在訪問共享資源的線程的優先級。

**歷史狀況:**在老版本的代碼中,做者是使用的OSSpinLock自旋鎖來保證線程安全,然後來因爲OSSpinLock的 bug 問題(存在潛在的優先級反轉BUG),做者將其替換成了pthread_mutex_t互斥鎖。

筆者的理解: 自動的遞歸修剪邏輯是這樣的:

- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}
複製代碼

_queue是一個串行隊列:

_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
複製代碼

能夠明確的是,自動修剪過程不存在線程安全問題,固然框架還暴露了修剪內存的方法給外部使用,那麼當外部在多線程調用修剪內存方法就可能會出現線程安全問題。

這裏作了一個 10ms 的掛起操做而後循環嘗試,直接捨棄了互斥鎖的空轉期,但這樣也避免了多線程訪問下過多的空轉佔用過多的 CPU 資源。做者這樣處理極可能加長了修剪內存的時間,可是卻避免了極限狀況下空轉對 CPU 的佔用。

顯然,做者是指望使用者在後臺線程修剪內存(最好使用者不去顯式的調用修剪內存方法)。

異步線程釋放資源

這裏做者使用了一個容器將要釋放的節點裝起來,而後在某個隊列(默認是非主隊列)裏面調用了一下該容器的方法。雖然看代碼可能不理解,可是做者寫了一句註釋release in queue:某個對象的方法最後在某個線程調用,這個對象就會在當前線程釋放。很明顯,這裏是做者將節點的釋放放其餘線程,從而減輕主線程的資源開銷。

##(3)檢查內存是否超限的定時任務 有這樣一段代碼:

- (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];
    });
}
複製代碼

能夠看到,做者是使用一個遞歸+延時來實現定時任務的,這裏能夠自定義檢測的時間間隔。

##(4)進入後臺和內存警告的處理 在該類初始化時,做者寫了內存警告和進入後臺兩個監聽:

[[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];
}
複製代碼

使用者還能夠經過閉包實時監聽。

##(5)讀數據

- (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;
}
複製代碼

邏輯很簡單,關鍵的一步是 node->_time = CACurrentMediaTime()[_lru bringNodeToHead:node] ;即更新這塊內存的時間,而後將該節點移動到鏈表頭部,實現了基於時間的優先級排序,爲 LRU 的實現提供了可靠的數據結構基礎。

##(6)寫數據 代碼有刪減,解析寫在代碼中:

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    ......
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
//1 若緩存中有:修改node的變量,將該節點移動到頭部
        ......
        [_lru bringNodeToHead:node];
    } else {
//2 若緩存中沒有,建立一個內存,將該節點插入到頭部
        node = [_YYLinkedMapNode new];
        ......
        [_lru insertNodeAtHead:node];
    }
//3 判斷是否須要修剪內存佔用,若須要:異步修剪,保證寫入的性能
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
//4 判斷是否須要修剪內存塊數量,若須要:默認在非主隊列釋放無用內存,保證寫入的性能
    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);
}
複製代碼

2、磁盤緩存:YYDiskCache

在暴露給用戶的 API 中,磁盤緩存的功能和內存緩存很像,一樣有讀寫數據和修剪數據等功能。

YYDiskCache的磁盤緩存處理性能很是優越,做者測試了數據庫和文件存儲的讀寫效率:iPhone 6 64G 下,SQLite 寫入性能比直接寫文件要高,但讀取性能取決於數據大小:當單條數據小於 20K 時,數據越小 SQLite 讀取性能越高;單條數據大於 20K 時,直接寫爲文件速度會更快一些。(更詳細的說明看文末連接)

因此做者對磁盤緩存的處理方式爲 SQLite 結合文件存儲的方式。

磁盤緩存的核心類是YYKVStorage,注意該類是非線程安全的,它主要封裝了 SQLite 數據庫的操做和文件存儲操做。

後文的剖析大部分的代碼都是在YYKVStorage文件中。

##(1)磁盤緩存的文件結構 首先,須要瞭解一下做者設計的在磁盤中的文件結構(在YYKVStorage.m中做者的註釋):

/*
 File:
 /path/
      /manifest.sqlite
      /manifest.sqlite-shm
      /manifest.sqlite-wal
      /data/
           /e10adc3949ba59abbe56e057f20f883e
           /e10adc3949ba59abbe56e057f20f883e
      /trash/
            /unused_file_or_folder
 
 SQL:
 create table if not exists manifest (
    key                 text,
    filename            text,
    size                integer,
    inline_data         blob,
    modification_time   integer,
    last_access_time    integer,
    extended_data       blob,
    primary key(key)
 ); 
 create index if not exists last_access_time_idx on manifest(last_access_time);
 */
複製代碼

path 是一個初始化時使用的變量,不一樣的 path 對應不一樣的數據庫。在 path 下面有 sqlite 數據庫相關的三個文件,以及兩個目錄(/data 和 /trash),這兩個目錄就是文件存儲方便直接讀取的地方,也就是爲了實現上文說的在高於某個臨界值時直接讀取文件比從數據庫讀取快的理論。

在數據庫中,建了一個表,表的結構如上代碼所示:

  1. key 惟一標識
  2. size 當前內存塊的大小。
  3. inline_data 使用者存儲內容(value)的二進制數據。
  4. last_access_time 最後訪問時間,便於磁盤緩存實現 LRU 算法的數據結構排序。
  5. filename 文件名,它指向直接存文件狀況下的文件名,具體交互請往下看~

如何實現 SQLite 結合文件存儲

這一個重點問題,就像以前說的,在某個臨界值時,直接讀取文件的效率要高於從數據庫讀取,第一反應多是寫文件和寫數據庫分離,也就是上面的結構中,manifest.sqlite 數據庫文件和 /data 文件夾內容無關聯,讓 /data 去存儲高於臨界值的數據,讓 sqlite 去存儲低於臨界值的數據。

然而這樣會帶來兩個問題:

  1. /data 目錄下的緩存數據沒法高速查找(可能只有遍歷)
  2. 沒法統一管理磁盤緩存

爲了完美處理該問題,做者將它們結合了起來,全部關於用戶存儲數據的相關信息都會放在數據庫中(即剛纔說的那個table中),而待存儲數據的二進制文件,卻根據狀況分別處理:要麼存在數據庫表的 inline_data 下,要麼直接存儲在 /data 文件夾下。

如此一來,一切問題迎刃而解,下文根據源碼進行驗證和探究。

##(2)數據庫表的OC模型體現 固然,爲了讓接口可讀性更高,做者寫了一個對應數據庫表的模型,做爲使用者實際業務使用的類:

@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;                ///< key
@property (nonatomic, strong) NSData *value;                ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size;                             ///< value's size in bytes @property (nonatomic) int modTime; ///< modification unix timestamp @property (nonatomic) int accessTime; ///< last access unix timestamp @property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data) @end 複製代碼

該類的屬性和數據庫表的鍵一一對應。

##(3)數據庫的操做封裝

對於 sqlite 的封裝比較常規,做者的容錯處理作得很好,下面就一些重點地方作一些講解,對數據庫操做感興趣的朋友能夠直接去看源碼。

sqlite3_stmt 緩存

YYKVStorage 類有這樣一個變量:CFMutableDictionaryRef _dbStmtCache; 經過 sql 生成 sqlite3_stmt 的封裝方法是這樣的:

- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
    if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL;
    sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql));
    if (!stmt) {
        int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
        if (result != SQLITE_OK) {
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
            return NULL;
        }
        CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt);
    } else {
        sqlite3_reset(stmt);
    }
    return stmt;
}
複製代碼

做者使用了一個 hash 容器來緩存 stmt, 每次根據 sql 生成 stmt 時,若已經存在緩存就執行一次 sqlite3_reset(stmt); 讓 stmt 回到初始狀態。

如此一來,提升了數據庫讀寫的效率,是一個小 tip。

利用 sql 語句操做數據庫實現 LRU

數據庫操做,仍然有根據佔用內存大小、最後訪問時間、內存塊數量進行修剪內存的方法,下面就根據最後訪問時間進行修剪方法作爲例子:

- (BOOL)_dbDeleteItemsWithTimeEarlierThan:(int)time {
    NSString *sql = @"delete from manifest where last_access_time < ?1;";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    if (!stmt) return NO;
    sqlite3_bind_int(stmt, 1, time);
    int result = sqlite3_step(stmt);
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled)  NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}
複製代碼

能夠看到,做者利用 sql 語句,很輕鬆的實現了內存的修剪。

寫入時的核心邏輯

寫入時,做者根據是否有 filename 判斷是否須要將寫入的數據二進制存入數據庫(代碼有刪減):

- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    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;
    
    ......
    if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    ....
}
複製代碼

若存在 filename ,雖然不會寫入數據庫,可是會直接寫入 /data 文件夾,這個邏輯是在本類的 public 方法中作的。

##(4)文件操做的封裝 主要是 NSFileManager 相關方法的基本使用,比較獨特的是,做者使用了一個「垃圾箱」,也就是磁盤文件存儲結構中的 /trash 目錄。

能夠看到兩個方法:

- (BOOL)_fileMoveAllToTrash {
    CFUUIDRef uuidRef = CFUUIDCreate(NULL);
    CFStringRef uuid = CFUUIDCreateString(NULL, uuidRef);
    CFRelease(uuidRef);
    NSString *tmpPath = [_trashPath stringByAppendingPathComponent:(__bridge NSString *)(uuid)];
    BOOL suc = [[NSFileManager defaultManager] moveItemAtPath:_dataPath toPath:tmpPath error:nil];
    if (suc) {
        suc = [[NSFileManager defaultManager] createDirectoryAtPath:_dataPath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    CFRelease(uuid);
    return suc;
}

- (void)_fileEmptyTrashInBackground {
    NSString *trashPath = _trashPath;
    dispatch_queue_t queue = _trashQueue;
    dispatch_async(queue, ^{
        NSFileManager *manager = [NSFileManager new];
        NSArray *directoryContents = [manager contentsOfDirectoryAtPath:trashPath error:NULL];
        for (NSString *path in directoryContents) {
            NSString *fullPath = [trashPath stringByAppendingPathComponent:path];
            [manager removeItemAtPath:fullPath error:NULL];
        }
    });
}
複製代碼

上面個方法是將 /data 目錄下的文件移動到 /trash 目錄下,下面個方法是將 /trash 目錄下的文件在異步線程清理掉。

**筆者的理解:**很容易想到,刪除文件是一個比較耗時的操做,因此做者把它放到了一個專門的隊列處理。而刪除的文件用一個專門的路徑 /trash 放置,避免了寫入數據和刪除數據之間發生衝突。試想,若刪除的邏輯和寫入的邏輯都是對 /data 目錄進行操做,而刪除邏輯比較耗時,那麼就會很容易出現誤刪等狀況。

##(5)YYDiskCache 對 YYKVStorage 的二次封裝 對於 YYKVStorage 類的公有方法,筆者不作解析,就是對數據庫操做和寫文件操做的一個結合封裝,很簡單一看便知。

做者不提倡直接使用非線程安全的 YYKVStorage 類,因此封裝了一個線程安全的 YYDiskCache 類便於你們使用。

因此,YYDiskCache 類中主要是作了一些操做磁盤緩存的線程安全機制,是基於信號量(dispatch_semaphore)來處理的,暴露的接口中相似 YYMemoryCache 類的一系列方法。

剩餘磁盤空間的限制

磁盤緩存中,多了一個以下修剪方法:

- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
    if (targetFreeDiskSpace == 0) return;
    int64_t totalBytes = [_kv getItemsSize];
    if (totalBytes <= 0) return;
    int64_t diskFreeBytes = _YYDiskSpaceFree();
    if (diskFreeBytes < 0) return;
    int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
    if (needTrimBytes <= 0) return;
    int64_t costLimit = totalBytes - needTrimBytes;
    if (costLimit < 0) costLimit = 0;
    [self _trimToCost:(int)costLimit];
}
複製代碼

根據剩餘的磁盤空間的限制進行修剪,做者確實想得很周到。_YYDiskSpaceFree()是做者寫的一個 c 方法,用於獲取剩餘磁盤空間。

MD5 加密 key

- (NSString *)_filenameForKey:(NSString *)key {
    NSString *filename = nil;
    if (_customFileNameBlock) filename = _customFileNameBlock(key);
    if (!filename) filename = _YYNSStringMD5(key);
    return filename;
}
複製代碼

filename 是做者根據使用者傳入的 key 作一次 MD5 加密所得的字符串,因此不要誤覺得文件名就是你傳入的 key (_YYNSStringMD5()是做者寫的一個加密方法)。固然,框架提供了一個 _customFileNameBlock 容許你自定義文件名。

同時提供同步和異步接口

能夠看到諸如此類的設計:

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

- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block {
    if (!block) return;
    __weak typeof(self) _self = self;
    dispatch_async(_queue, ^{
        __strong typeof(_self) self = _self;
        BOOL contains = [self containsObjectForKey:key];
        block(key, contains);
    });
}
複製代碼

因爲可能存儲的文件過大,在讀寫時會佔用過多的資源,因此做者對於這些操做都分別提供了同步和異步的接口,可謂很是人性化,這也是接口設計的一些值得學習的地方。

3、綜合封裝:YYCache

實際上上文的剖析已經囊括了 YYCache 框架的核心了。YYCache 類主要是對內存緩存和磁盤緩存的結合封裝,代碼很簡單,有一點須要提出來:

- (void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, id<NSCoding> object))block {
    if (!block) return;
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (object) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            block(key, object);
        });
    } else {
        [_diskCache objectForKey:key withBlock:^(NSString *key, id<NSCoding> object) {
            if (object && ![_memoryCache objectForKey:key]) {
                [_memoryCache setObject:object forKey:key];
            }
            block(key, object);
        }];
    }
}
複製代碼

優先查找內存緩存_memoryCache中的數據,若查不到,就查詢磁盤緩存_diskCache,查詢磁盤緩存成功,將數據同步到內存緩存中,方便下次查找。

這麼作的理由很簡單:根據機械原理,較大的存儲設備要比較小的存儲設備運行得慢,而快速設備的造價遠高於低速設備。因此內存緩存的讀寫速度遠高於磁盤緩存。這也是開發中緩存設計的核心問題,咱們既要保證緩存讀寫的效率,又要考慮到空間佔用,其實又回到了空間和時間的權衡問題了。

寫在後面

YYCache 核心邏輯思路、接口設計、代碼組織架構、容錯處理、性能優化、內存管理、線程安全這些方面都作得很好很極致,閱讀起來很是舒服。

閱讀開源框架,第一步必定是通讀一下 API 瞭解該框架是幹什麼的,而後採用「分治」的思路逐個擊破,類比「歸併算法」:先拆開再合併,切勿想一口吃成胖子,特別是對於某些「重量級」框架。

但願讀者朋友們閱讀事後有所收穫😁。

參考文獻:做者 ibireme 的博客 YYCache 設計思路

相關文章
相關標籤/搜索