前言:準備看下YY系列中的YYWebImage
框架,發現該框架是使用YYCache
來做快取的。那就從快取開始吧.
先奉上YYCache
框架的地址以及作者的設計思路
學習YYCache
框架你可以get到:
1.優雅的程式碼風格
2.優秀的介面設計
3.YYCache的層次結構
4.YYMemoryCache類的層次結構和快取機制
5.YYDiskCache類的層次結構和快取機制
YYCache
YYCache
最為食物鏈的最頂端的男人,並沒有什麼好說的,所以我們就從YYMemoryCache
和YYDiskCache
開始吧。
YYMemoryCache
YYMemoryCache
記憶體儲存是的原理是利用CFDictionary
物件的 key-value
開闢記憶體儲存機制和雙向連結串列原理來實現LRU演算法。這裡是官方文件對CFDictionary
的解釋:
1 |
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
初始化的時候會建立空的私有物件YYLinkedMap
連結串列,接下來所有的操作其實就是對這個連結串列的操作。當然,YYMemoryCache
提供了一個定時器介面給你,你可以通過設定autoTrimInterval
屬性去完成每隔一定時間去檢查countLimit
,costLimit
是否達到了最大限制,並做相應的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (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
節點的操作。下圖綠色部分代表節點:
下圖是連結串列節點的結構圖:
現在我們先來看如何去構造一個連結串列新增節點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
- (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); } |
你可以點選這裡自己去操作雙向連結串列
連結串列移除節點的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (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); } |
YYMemoryCache
類還為我們提供了下列介面方便我們呼叫:
1 2 3 |
- (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
來確定是那種寫入方式:
1 2 3 4 5 6 7 8 9 10 11 |
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欄位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (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]; } } |
插入或者是更新資料庫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
- (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
物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (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刪除掉資料庫就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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的陣列。但是其實都大同小異的流程,就不一一累述了。上個圖吧:
YYDiskCache
YYDiskCache
類有兩種初始化方式:
1 2 3 |
- (nullable instancetype)initWithPath:(NSString *)path; - (nullable instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold |
YYDiskCache
類持有一個YYKVStorage
物件,但是你不能手動的去控制YYKVStorage
物件的YYKVStorageType
。YYDiskCache
類初始化提供一個threshold
的引數,預設的為20KB。然後根據這個值得大小來確定YYKVStorageType
的型別。
1 2 3 4 5 6 7 8 9 |
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
儲存方法,下面是實現程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
- (void)setObject:(id)object forKey:(NSString *)key { 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 { <a href='http://www.jobbole.com/members/xyz937134366'>@try</a> { //序列化 value = [NSKeyedArchiver archivedDataWithRootObject:object]; } <a href='http://www.jobbole.com/members/wx895846013'>@catch</a> (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屬性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
- (id)objectForKey:(NSString *)key { 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 { <a href='http://www.jobbole.com/members/xyz937134366'>@try</a> { object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value]; } <a href='http://www.jobbole.com/members/wx895846013'>@catch</a> (NSException *exception) { // nothing to do... } } if (object && item.extendedData) { [YYDiskCache setExtendedData:item.extendedData toObject:object]; } return object; } |
刪除
刪除操作就是直接呼叫的YYKVStorage
物件來操作了。
1 2 3 4 5 6 |
- (void)removeObjectForKey:(NSString *)key { if (!key) return; Lock(); [_kv removeItemForKey:key]; Unlock(); } |
當然,YYDiskCache
和YYMemoryCache
一樣也給你提供了一些類似limit
的介面供你操作。
1 2 3 |
- (void)trimToCount:(NSUInteger)count; - (void)trimToCost:(NSUInteger)cost; - (void)trimToAge:(NSTimeInterval)age; |
和YYKVStorage
不一樣的是,作為更高層的YYDiskCache
是一個執行緒安全的類。你應該使用YYDiskCache
而不是YYKVStorage
。
最後再帶一筆食物端最頂端的男人YYCache
,當他寫入的時候會同時呼叫YYDiskCache
磁碟操作和YYMemoryCache
記憶體操作。讀取的時候先從記憶體讀取,因為在記憶體的讀取速度比磁碟快很多,如果沒有讀取到資料才會去磁碟讀取。
讀後感只有四個字:
如沐春風