原文
前言:准備看下YY系列中的YYWebImage框架,發現該框架是使用YYCache來做緩存的。那就從緩存開始吧.
先奉上YYCache框架的地址以及作者的設計思路
學習YYCache框架你可以get到:
1.優雅的代碼風格
2.優秀的接口設計
3.YYCache的層次結構
4.YYMemoryCache類的層次結構和緩存機制
5.YYDiskCache類的層次結構和緩存機制
YYCache
YYCache結構
YYCache最為食物鏈的最頂端,並沒有什麼好說的,所以我們就從YYMemoryCache和YYDiskCache開始吧。
YYMemoryCache
YYMemoryCache內存儲存是的原理是利用CFDictionary對象的 key-value開辟內存儲存機制和雙向鏈表原理來實現LRU算法。這裡是官方文檔對CFDictionary的解釋:
CFMutableDictionary creates dynamic dictionaries where you can add or delete key-value pairs at any time, and the dictionary automatically allocates memory as needed.
YYMemoryCache類結構圖
YYMemoryCache初始化的時候會建立空的私有對象YYLinkedMap鏈表,接下來所有的操作其實就是對這個鏈表的操作。當然,YYMemoryCache提供了一個定時器接口給你,你可以通過設置autoTrimInterval屬性去完成每隔一定時間去檢查countLimit,costLimit是否達到了最大限制,並做相應的操作。
- (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]; }); } - (void)_trimInBackground { dispatch_async(_queue, ^{ //檢查是否達到設置的最大消耗,並做相應的處理 [self _trimToCost:self->_costLimit]; //檢查是否達到該緩存設置的最大持有對象數,並做相應的處理 [self _trimToCount:self->_countLimit]; //當前的時間和鏈表最後的節點時間的差值是否大於設定的_ageLimit值,移除大於該值得節點 [self _trimToAge:self->_ageLimit]; }); }
YYMemoryCache以block的形式給你提供了下面接口:
didReceiveMemoryWarningBlock(當app接受到內存警告)
didEnterBackgroundBlock (當app進入到後台)
當然,你也可以通過設置相應的shouldRemoveAllObjectsOnMemoryWarning和 shouldRemoveAllObjectsWhenEnteringBackground值來移除YYMemoryCache持有的鏈表。
下面我們來看看YYMemoryCache類的增,刪,查等操作。在這之前我們先看看YYLinkedMap這個類。
1.YYLinkedMap內部結構
YYLinkedMap作為雙向鏈表,主要的工作是為YYMemoryCache類提供對YYLinkedMapNode節點的操作。下圖綠色部分代表節點:
雙向鏈表結構
下圖是鏈表節點的結構圖:
鏈表節點
現在我們先來看如何去構造一個鏈表添加節點:
setObject
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { if (!key) return; if (!object) { [self removeObjectForKey:key]; return; } //鎖 pthread_mutex_lock(&_lock); //查找是否存在對應該key的節點 _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); NSTimeInterval now = CACurrentMediaTime(); if (node) { //修改相應的數據 _lru->_totalCost -= node->_cost; _lru->_totalCost += cost; node->_cost = cost; node->_time = now; node->_value = object; //根據LRU算法原理,將訪問的點移到最前面 [_lru bringNodeToHead:node]; } else { node = [_YYLinkedMapNode new]; node->_cost = cost; node->_time = now; node->_key = key; node->_value = object; //在鏈表最前面插入結點 [_lru insertNodeAtHead:node]; } //判斷鏈表的消耗的總資源是否大於設置的最大值 if (_lru->_totalCost > _costLimit) { dispatch_async(_queue, ^{ [self trimToCost:_costLimit]; }); } //判斷鏈表的總持有節點是否大於該緩存設置的最大持有數 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); }
你可以點擊這裡自己去操作雙向鏈表
addNode.gif
鏈表移除節點的操作:
- (void)removeObjectForKey:(id)key { if (!key) return; //鎖 pthread_mutex_lock(&_lock); //根據key拿到相應的節點 _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); }
removeNode.gif
YYMemoryCache類還為我們提供了下列接口方便我們調用:
- (BOOL)containsObjectForKey:(id)key; - (nullable id)objectForKey:(id)key; - (void)removeAllObjects;
總結:YYMemoryCache是利用key-value機制內存緩存類,所有的方法都是線程安全的。如果你熟悉NSCache類,你會發現兩者的接口很是相似。
當然YYMemoryCache有著自己的特點:
1.YYMemoryCache采用LRU(least-recently-used)算法來移除節點。
2.YYMemoryCache可以用countLimit,costLimit,ageLimit屬性做相應的控制。
3.YYMemoryCache類可以設置相應的屬性來控制退到後台或者接受到內存警告的時候移除鏈表。
YYKVStorage
YYKVStorage是一個基於sql數據庫和文件寫入的緩存類,注意它並不是線程安全。你可以自己定義YYKVStorageType來確定是那種寫入方式:
typedef NS_ENUM(NSUInteger, YYKVStorageType) { /// The `value` is stored as a file in file system. YYKVStorageTypeFile = 0, /// The `value` is stored in sqlite with blob type. YYKVStorageTypeSQLite = 1, /// The `value` is stored in file system or sqlite based on your choice. YYKVStorageTypeMixed = 2, };
1.寫入和更新
我們看看Demo中直接用YYKVStorage儲存NSNumber和NSData YYKVStorageTypeFile和YYKVStorageTypeSQLite類型所用的時間:
你可以發現在儲存小型數據NSNumberYYKVStorageTypeFile類型是YYKVStorageTypeSQLite大約4倍多,而在大型數據的時候兩者的表現是相反的。顯然選擇合適的儲存方式是很有必要的。
這裡需要提醒的事:
Demo中YYKVStorageTypeFile類型其實不僅寫入了本地文件也同時寫入了數據庫,只不過數據庫裡面存的是除了value值以外的key, filename, size, inline_data(NULL), modification_time , last_access_time, extended_data字段。
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData { if (key.length == 0 || value.length == 0) return NO; //_type為YYKVStorageTypeSQLite時候filename應該為空,不然還是會寫入文件 //_type為YYKVStorageTypeFile時候filename的值不能為空 if (_type == YYKVStorageTypeFile && filename.length == 0) { return NO; } //是否寫入文件是根據filename.length長度來判斷的 if (filename.length) { //先儲存在文件裡面 if (![self _fileWriteWithName:filename data:value]) { return NO; } //儲存在sql數據庫 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]; } } //儲存在sql數據庫 return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData]; } }
插入或者是更新數據庫
- (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; int timestamp = (int)time(NULL); //sqlite3_bind_xxx函數給這條語句綁定參數 sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); sqlite3_bind_int(stmt, 3, (int)value.length); //當fileName為空的時候存在數據庫的是value.bytes,不然存的是NULl對象 if (fileName.length == 0) { sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0); } else { sqlite3_bind_blob(stmt, 4, NULL, 0, 0); } sqlite3_bind_int(stmt, 5, timestamp); sqlite3_bind_int(stmt, 6, timestamp); sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); //通過sqlite3_step命令執行創建表的語句 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; }
2.讀取
我們嘗試的去緩存裡面拿取數據,我們發現當為YYKVStorage對象type不同,存取的方式不同所以讀取的方式也不同:
1.因為在插入的時候我們就說了,當為YYKVStorageTypeFile類型的時候數據是存在本地文件的其他存在數據庫。所以YYKVStorage對象先根據key從數據庫拿到數據然後包裝成YYKVStorageItem對象,然後再根據filename讀取本地文件數據賦給YYKVStorageItem對象的value屬性。
2.當為YYKVStorageTypeSQLite類型就是直接從數據庫把所有數據都讀出來賦給YYKVStorageItem對象。
- (YYKVStorageItem *)getItemForKey:(NSString *)key { if (key.length == 0) return nil; /*先從數據庫讀包裝item, 當時filename不為空的時候,以為著數據庫裡面沒有存Value值,還得去文件裡面讀出來value值 當時filename為空的時候,意味著直接從數據庫來拿取Value值 */ YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO]; if (item) { //更新的last_access_time字段 [self _dbUpdateAccessTimeWithKey:key]; if (item.filename) { //從文件裡面讀取value值 item.value = [self _fileReadWithName:item.filename]; if (!item.value) { //數據為空則從數據庫刪除這條記錄 [self _dbDeleteItemWithKey:key]; item = nil; } } } return item; }
3.刪除
YYKVStorage的type當為YYKVStorageTypeFile類型是根據key將本地和數據庫都刪掉,而YYKVStorageTypeSQLite是根據key刪除掉數據庫就好了。
- (BOOL)removeItemForKey:(NSString *)key { if (key.length == 0) return NO; switch (_type) { case YYKVStorageTypeSQLite: { return [self _dbDeleteItemWithKey:key]; } break; case YYKVStorageTypeFile: case YYKVStorageTypeMixed: { NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { [self _fileDeleteWithName:filename]; } return [self _dbDeleteItemWithKey:key]; } break; default: return NO; } }
我們這裡分別列取了增刪改查的單個key的操作,你還可以去批量的去操作key的數組。但是其實都大同小異的流程,就不一一累述了。上個圖吧:
這個類也就看的差不多了,但是要注意的事,YYCache作者並不希望我們直接使用這個類,而是使用更高層的YYDiskCache類。那我們就繼續往下面看吧。
YYDiskCache
YYDiskCache類有兩種初始化方式:
- (nullable instancetype)initWithPath:(NSString *)path; - (nullable instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold
YYDiskCache類持有一個YYKVStorage對象,但是你不能手動的去控制YYKVStorage對象的YYKVStorageType。YYDiskCache類初始化提供一個threshold的參數,默認的為20KB。然後根據這個值得大小來確定YYKVStorageType的類型。
YYKVStorageType type; if (threshold == 0) { type = YYKVStorageTypeFile; } else if (threshold == NSUIntegerMax) { type = YYKVStorageTypeSQLite; } else { type = YYKVStorageTypeMixed; } YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
因為YYDiskCache類的操作其實就是去操作持有的YYKVStorage對象,所以下面的部分會比較建簡略。
寫入和更新
在調用YYKVStorage對象的儲存操作前主要做了下面幾項操作:
1.key和object的判空容錯機制
2.利用runtime機制去取extendedData數據
3.根據是否定義了_customArchiveBlock來判斷選擇序列化object還是block回調得到value
4.value的判空容錯機制
5.根據YYKVStorage的type判斷以及_inlineThreshold和value值得長度來判斷是否選擇以文件的形式儲存value值。上面我們說過當value比較大的時候文件儲存速度比較快速。
6.如果_customFileNameBlock為空,則根據key通過md5加密得到轉化後的filename.不然直接拿到_customFileNameBlock關聯的filename。生成以後操作文件的路徑
做完上面的操作則直接調用YYKVStorage儲存方法,下面是實現代碼:
- (void)setObject:(id(NSCoding))object forKey:(NSString *)key { (因識別問題,此處(NSCoding)替換) if (!key) return; if (!object) { [self removeObjectForKey:key]; return; } //runtime 取extended_data_key的value NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object]; NSData *value = nil; if (_customArchiveBlock) { //block返回 value = _customArchiveBlock(object); } else { @try { //序列化 value = [NSKeyedArchiver archivedDataWithRootObject:object]; } @catch (NSException *exception) { // nothing to do... } } if (!value) return; NSString *filename = nil; if (_kv.type != YYKVStorageTypeSQLite) { //長度判斷這個儲存方式,value.length當大於_inlineThreshold則文件儲存 if (value.length > _inlineThreshold) { //將key 進行md5加密 filename = [self _filenameForKey:key]; } } Lock(); [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; Unlock(); }
讀取
讀取操作一般都是和寫入操作相輔相成的,我們來看看在調用YYKVStorage對象的讀取操作後做了哪些操作:
1.item.value的判空容錯機制
2.根據_customUnarchiveBlock值來判斷是直接將item.value block回調還是反序列化成object
3.根據object && item.extendedData 來決定是否runtime添加extended_data_key屬性
- (id(NSCoding))objectForKey:(NSString *)key { (因識別問題,此處(NSCoding)替換) if (!key) return nil; Lock(); YYKVStorageItem *item = [_kv getItemForKey:key]; Unlock(); if (!item.value) return nil; id object = nil; if (_customUnarchiveBlock) { object = _customUnarchiveBlock(item.value); } else { @try { object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value]; } @catch (NSException *exception) { // nothing to do... } } if (object && item.extendedData) { [YYDiskCache setExtendedData:item.extendedData toObject:object]; } return object; }
刪除
刪除操作就是直接調用的YYKVStorage對象來操作了。
- (void)removeObjectForKey:(NSString *)key { if (!key) return; Lock(); [_kv removeItemForKey:key]; Unlock(); }
當然,YYDiskCache和YYMemoryCache一樣也給你提供了一些類似limit的接口供你操作。
- (void)trimToCount:(NSUInteger)count;- (void)trimToCost:(NSUInteger)cost;- (void)trimToAge:(NSTimeInterval)age;
總結:和YYKVStorage不一樣的是,作為更高層的YYDiskCache是一個線程安全的類。你應該使用YYDiskCache而不是YYKVStorage。
讀後感只有四個字:
如沐春風