前言:準(zhǔn)備看下YY系列中的YYWebImage
框架,發(fā)現(xiàn)該框架是使用YYCache
來做緩存的土至。那就從緩存開始吧.
先奉上YYCache
框架的地址以及作者的設(shè)計(jì)思路
學(xué)習(xí)YYCache
框架你可以get到:
1.優(yōu)雅的代碼風(fēng)格
2.優(yōu)秀的接口設(shè)計(jì)
3.YYCache的層次結(jié)構(gòu)
4.YYMemoryCache類的層次結(jié)構(gòu)和緩存機(jī)制
5.YYDiskCache類的層次結(jié)構(gòu)和緩存機(jī)制
YYCache
YYCache
最為食物鏈的最頂端的男人,并沒有什么好說的猾昆,所以我們就從YYMemoryCache
和YYDiskCache
開始吧陶因。
YYMemoryCache
YYMemoryCache
內(nèi)存儲存是的原理是利用CFDictionary
對象的 key-value
開辟內(nèi)存儲存機(jī)制和雙向鏈表原理來實(shí)現(xiàn)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
初始化的時候會建立空的私有對象YYLinkedMap
鏈表垂蜗,接下來所有的操作其實(shí)就是對這個鏈表的操作坑赡。當(dāng)然,YYMemoryCache
提供了一個定時器接口給你么抗,你可以通過設(shè)置autoTrimInterval
屬性去完成每隔一定時間去檢查countLimit
毅否,costLimit
是否達(dá)到了最大限制,并做相應(yīng)的操作蝇刀。
- (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];
//遞歸的調(diào)用
[self _trimRecursively];
});
}
- (void)_trimInBackground {
dispatch_async(_queue, ^{
//檢查是否達(dá)到設(shè)置的最大消耗,并做相應(yīng)的處理
[self _trimToCost:self->_costLimit];
//檢查是否達(dá)到該緩存設(shè)置的最大持有對象數(shù),并做相應(yīng)的處理
[self _trimToCount:self->_countLimit];
//當(dāng)前的時間和鏈表最后的節(jié)點(diǎn)時間的差值是否大于設(shè)定的_ageLimit值螟加,移除大于該值得節(jié)點(diǎn)
[self _trimToAge:self->_ageLimit];
});
}
YYMemoryCache
以block的形式給你提供了下面接口:
- didReceiveMemoryWarningBlock(當(dāng)app接受到內(nèi)存警告)
- didEnterBackgroundBlock (當(dāng)app進(jìn)入到后臺)
當(dāng)然,你也可以通過設(shè)置相應(yīng)的shouldRemoveAllObjectsOnMemoryWarning
和 shouldRemoveAllObjectsWhenEnteringBackground
值來移除YYMemoryCache
持有的鏈表吞琐。
下面我們來看看YYMemoryCache
類的增捆探,刪,查等操作站粟。在這之前我們先看看YYLinkedMap
這個類黍图。
1.YYLinkedMap內(nèi)部結(jié)構(gòu)
YYLinkedMap
作為雙向鏈表,主要的工作是為YYMemoryCache
類提供對YYLinkedMapNode
節(jié)點(diǎn)的操作奴烙。下圖綠色部分代表節(jié)點(diǎn):
下圖是鏈表節(jié)點(diǎn)的結(jié)構(gòu)圖:
現(xiàn)在我們先來看如何去構(gòu)造一個鏈表添加節(jié)點(diǎn):
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
//鎖
pthread_mutex_lock(&_lock);
//查找是否存在對應(yīng)該key的節(jié)點(diǎn)
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
if (node) {
//修改相應(yīng)的數(shù)據(jù)
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
//根據(jù)LRU算法原理助被,將訪問的點(diǎn)移到最前面
[_lru bringNodeToHead:node];
} else {
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
//在鏈表最前面插入結(jié)點(diǎn)
[_lru insertNodeAtHead:node];
}
//判斷鏈表的消耗的總資源是否大于設(shè)置的最大值
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//判斷鏈表的總持有節(jié)點(diǎn)是否大于該緩存設(shè)置的最大持有數(shù)
if (_lru->_totalCount > _countLimit) { //當(dāng)超出設(shè)定的最大的值
//移除鏈表最后的節(jié)點(diǎn)
_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);
}
你可以點(diǎn)擊這里自己去操作雙向鏈表
鏈表移除節(jié)點(diǎn)的操作:
- (void)removeObjectForKey:(id)key {
if (!key) return;
//鎖
pthread_mutex_lock(&_lock);
//根據(jù)key拿到相應(yīng)的節(jié)點(diǎn)
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
[_lru removeNode:node];
//決定在哪個隊(duì)列里做釋放操作
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
類還為我們提供了下列接口方便我們調(diào)用:
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)removeAllObjects;
總結(jié):YYMemoryCache
是利用key-value機(jī)制內(nèi)存緩存類剖张,所有的方法都是線程安全的。如果你熟悉NSCache
類揩环,你會發(fā)現(xiàn)兩者的接口很是相似搔弄。
當(dāng)然YYMemoryCache
有著自己的特點(diǎn):
1.YYMemoryCache
采用LRU(least-recently-used)算法來移除節(jié)點(diǎn)。
2.YYMemoryCache
可以用countLimit
丰滑,costLimit
顾犹,ageLimit
屬性做相應(yīng)的控制。
3.YYMemoryCache
類可以設(shè)置相應(yīng)的屬性來控制退到后臺或者接受到內(nèi)存警告的時候移除鏈表褒墨。
YYKVStorage
YYKVStorage
是一個基于sql數(shù)據(jù)庫和文件寫入的緩存類炫刷,注意它并不是線程安全。你可以自己定義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
類型所用的時間:
你可以發(fā)現(xiàn)在儲存小型數(shù)據(jù)NSNumber
YYKVStorageTypeFile
類型是YYKVStorageTypeSQLite
大約4倍多郁妈,而在大型數(shù)據(jù)的時候兩者的表現(xiàn)是相反的柬唯。顯然選擇合適的儲存方式是很有必要的。這里需要提醒的事:
Demo
中YYKVStorageTypeFile
類型其實(shí)不僅寫入了本地文件也同時寫入了數(shù)據(jù)庫圃庭,只不過數(shù)據(jù)庫里面存的是除了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應(yīng)該為空球拦,不然還是會寫入文件
//_type為YYKVStorageTypeFile時候filename的值不能為空
if (_type == YYKVStorageTypeFile && filename.length == 0) {
return NO;
}
//是否寫入文件是根據(jù)filename.length長度來判斷的
if (filename.length) {
//先儲存在文件里面
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
//儲存在sql數(shù)據(jù)庫
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
//儲存數(shù)據(jù)庫失敗就刪除之前儲存的文件
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
//儲存在sql數(shù)據(jù)庫
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
插入或者是更新數(shù)據(jù)庫
- (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函數(shù)給這條語句綁定參數(shù)
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);
//當(dāng)fileName為空的時候存在數(shù)據(jù)庫的是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命令執(zhí)行創(chuàng)建表的語句
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.讀取
我們嘗試的去緩存里面拿取數(shù)據(jù)卜录,我們發(fā)現(xiàn)當(dāng)為YYKVStorage
對象type不同恕曲,存取的方式不同所以讀取的方式也不同:
1.因?yàn)樵诓迦氲臅r候我們就說了伊诵,當(dāng)為YYKVStorageTypeFile
類型的時候數(shù)據(jù)是存在本地文件的其他存在數(shù)據(jù)庫墓陈。所以YYKVStorage
對象先根據(jù)key從數(shù)據(jù)庫拿到數(shù)據(jù)然后包裝成YYKVStorageItem
對象呆奕,然后再根據(jù)filename
讀取本地文件數(shù)據(jù)賦給YYKVStorageItem
對象的value屬性梢杭。
2.當(dāng)為YYKVStorageTypeSQLite
類型就是直接從數(shù)據(jù)庫把所有數(shù)據(jù)都讀出來賦給YYKVStorageItem
對象儒旬。
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
/*先從數(shù)據(jù)庫讀包裝item栏账,
當(dāng)時filename不為空的時候,以為著數(shù)據(jù)庫里面沒有存Value值栈源,還得去文件里面讀出來value值
當(dāng)時filename為空的時候挡爵,意味著直接從數(shù)據(jù)庫來拿取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) {
//數(shù)據(jù)為空則從數(shù)據(jù)庫刪除這條記錄
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
3.刪除
YYKVStorage
的type當(dāng)為YYKVStorageTypeFile
類型是根據(jù)key將本地和數(shù)據(jù)庫都刪掉,而YYKVStorageTypeSQLite
是根據(jù)key刪除掉數(shù)據(jù)庫就好了。
- (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的數(shù)組茶鹃。但是其實(shí)都大同小異的流程,就不一一累述了艰亮。上個圖吧:
這個類也就看的差不多了闭翩,但是要注意的事,YYCache作者并不希望我們直接使用這個類迄埃,而是使用更高層的
YYDiskCache
類疗韵。那我們就繼續(xù)往下面看吧。
YYDiskCache
YYDiskCache
類有兩種初始化方式:
- (nullable instancetype)initWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold
YYDiskCache
類持有一個YYKVStorage
對象侄非,但是你不能手動的去控制YYKVStorage
對象的YYKVStorageType
蕉汪。YYDiskCache
類初始化提供一個threshold
的參數(shù)流译,默認(rèn)的為20KB。然后根據(jù)這個值得大小來確定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];
因?yàn)?code>YYDiskCache類的操作其實(shí)就是去操作持有的YYKVStorage
對象先蒋,所以下面的部分會比較建簡略骇钦。
寫入和更新
在調(diào)用YYKVStorage
對象的儲存操作前主要做了下面幾項(xiàng)操作:
1.key和object的判空容錯機(jī)制
2.利用runtime機(jī)制去取extendedData數(shù)據(jù)
3.根據(jù)是否定義了_customArchiveBlock來判斷選擇序列化object還是block回調(diào)得到value
4.value的判空容錯機(jī)制
5.根據(jù)YYKVStorage
的type判斷以及_inlineThreshold和value值得長度來判斷是否選擇以文件的形式儲存value值宛渐。上面我們說過當(dāng)value比較大的時候文件儲存速度比較快速。
6.如果_customFileNameBlock為空眯搭,則根據(jù)key通過md5加密得到轉(zhuǎn)化后的filename.不然直接拿到_customFileNameBlock關(guān)聯(lián)的filename窥翩。生成以后操作文件的路徑
做完上面的操作則直接調(diào)用YYKVStorage
儲存方法,下面是實(shí)現(xiàn)代碼:
- (void)setObject:(id<NSCoding>)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 {
@try {
//序列化
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (!value) return;
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
//長度判斷這個儲存方式鳞仙,value.length當(dāng)大于_inlineThreshold則文件儲存
if (value.length > _inlineThreshold) {
//將key 進(jìn)行md5加密
filename = [self _filenameForKey:key];
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
讀取
讀取操作一般都是和寫入操作相輔相成的寇蚊,我們來看看在調(diào)用YYKVStorage
對象的讀取操作后做了哪些操作:
1.item.value的判空容錯機(jī)制
2.根據(jù)_customUnarchiveBlock值來判斷是直接將item.value block回調(diào)還是反序列化成object
3.根據(jù)object && item.extendedData 來決定是否runtime添加extended_data_key屬性
- (id<NSCoding>)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 {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
return object;
}
刪除
刪除操作就是直接調(diào)用的YYKVStorage
對象來操作了。
- (void)removeObjectForKey:(NSString *)key {
if (!key) return;
Lock();
[_kv removeItemForKey:key];
Unlock();
}
當(dāng)然棍好,YYDiskCache
和YYMemoryCache
一樣也給你提供了一些類似limit
的接口供你操作仗岸。
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
和YYKVStorage
不一樣的是,作為更高層的YYDiskCache
是一個線程安全的類借笙。你應(yīng)該使用YYDiskCache
而不是YYKVStorage
扒怖。
最后再帶一筆食物端最頂端的男人YYCache
,當(dāng)他寫入的時候會同時調(diào)用YYDiskCache
磁盤操作和YYMemoryCache
內(nèi)存操作业稼。讀取的時候先從內(nèi)存讀取盗痒,因?yàn)樵趦?nèi)存的讀取速度比磁盤快很多,如果沒有讀取到數(shù)據(jù)才會去磁盤讀取低散。
讀后感只有四個字:
如沐春風(fēng)