系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點(diǎn)
YYModel 源碼剖析:關(guān)注性能
YYImage 源碼剖析:圖片處理技巧
YYWebImage 源碼剖析:線程處理與緩存策略
寫(xiě)在前面
YYCache 作為當(dāng)下 iOS 圈最流行的緩存框架,有著優(yōu)越的性能和絕佳的設(shè)計(jì)。筆者花了些時(shí)間對(duì)其“解剖”了一番,發(fā)現(xiàn)了很多有意思的東西,所以寫(xiě)下本文分享一下。
考慮到篇幅,筆者對(duì)于源碼的解析不會(huì)過(guò)多的涉及 API 使用和一些基礎(chǔ)知識(shí),更多的是剖析作者 ibireme 的設(shè)計(jì)思維和重要技術(shù)實(shí)現(xiàn)細(xì)節(jié)。
YYCache 主要分為兩部分:內(nèi)存緩存和磁盤(pán)緩存(對(duì)應(yīng) YYMemoryCache 和 YYDiskCache)。在日常開(kāi)發(fā)業(yè)務(wù)使用中,多是直接操作 YYCache 類,該類是對(duì)內(nèi)存緩存功能和磁盤(pán)緩存功能的一個(gè)簡(jiǎn)單封裝。
源碼基于 1.0.4 版本。
一、內(nèi)存緩存:YYMemoryCache
總覽 API ,會(huì)發(fā)現(xiàn)一些見(jiàn)名知意的方法:
- (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;
......
可以看出,該類主要包含讀寫(xiě)功能和修剪功能(修剪是為了控制內(nèi)存緩存的大小等)。當(dāng)然,還有其他一些自定義方法,比如釋放操作的線程選擇、內(nèi)存警告和進(jìn)入后臺(tái)時(shí)是否清除內(nèi)存緩存等。
對(duì)該類的基本功能有了了解之后,就可以直接切實(shí)現(xiàn)源碼了。
(1)LRU 緩存淘汰算法
既然有修剪緩存的功能,必然涉及到一個(gè)緩存淘汰算法,YYMemoryCache 和 YYDiskCache 都是實(shí)現(xiàn)的 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;
}
熟悉鏈表的朋友應(yīng)該一眼就看出來(lái)貓膩,作者是使用的一個(gè)雙向鏈表+散列容器來(lái)實(shí)現(xiàn) LRU 的。
鏈表的節(jié)點(diǎn) (_YYLinkedMapNode):
- 同時(shí)使用前驅(qū)和后繼指針(即
_prev和_next)是為了快速找到前驅(qū)和后繼節(jié)點(diǎn)。 - 這里使用
__unsafe_unretained而不使用__weak。雖然兩者都不會(huì)持有指針?biāo)赶虻膶?duì)象,但是在指向?qū)ο筢尫艜r(shí),前者并不會(huì)自動(dòng)置空指針,形成野指針,不過(guò)經(jīng)過(guò)筆者后面的閱讀,發(fā)現(xiàn)作者避免了野指針的出現(xiàn);而且從性能層面看(作者原話):訪問(wèn)具有 __weak 屬性的變量時(shí),實(shí)際上會(huì)調(diào)用objc_loadWeak()和objc_storeWeak()來(lái)完成,這也會(huì)帶來(lái)很大的開(kāi)銷(xiāo),所以要避免使用__weak屬性。 -
_key和_value就是框架使用者想要存儲(chǔ)的鍵值對(duì),可以看出作者的設(shè)計(jì)是一個(gè)鍵值對(duì)對(duì)應(yīng)一個(gè)節(jié)點(diǎn)(_YYLinkedMapNode)。 -
_cost和_time表示該節(jié)點(diǎn)的內(nèi)存大小和最后訪問(wèn)的時(shí)間。
LRU 實(shí)現(xiàn)類 (_YYLinkedMap) :
- 包含頭尾指針(
_head和_tail),保證雙端查詢的效率。 -
_totalCost和_totalCount記錄最大內(nèi)存占用限制和數(shù)量限制。 -
_releaseOnMainThread和_releaseAsynchronously分別表示在主線程釋放和在異步線程釋放,它們的實(shí)現(xiàn)后文會(huì)講到。 -
_dic變量是 OC 開(kāi)發(fā)中常用的散列容器,所有節(jié)點(diǎn)都會(huì)在_dic中以 key-value 的形式存在,保證常數(shù)級(jí)查詢效率。
既然是 LRU 算法,怎么能只有數(shù)據(jù)結(jié)構(gòu),往下面看 _YYLinkedMap 類實(shí)現(xiàn)了如下算法(嗯,挺常規(guī)的節(jié)點(diǎn)操作,代碼質(zhì)量挺高的,就不說(shuō)明實(shí)現(xiàn)了):
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
現(xiàn)在 LRU 的數(shù)據(jù)結(jié)構(gòu)和操作算法實(shí)現(xiàn)都有了,就可以看具體的業(yè)務(wù)了。
(2)修剪內(nèi)存的邏輯
正如一開(kāi)始貼的 API ,該類有三種修剪內(nèi)存的依據(jù):根據(jù)緩存的內(nèi)存塊數(shù)量、根據(jù)占用內(nèi)存大小、根據(jù)是否是最近使用。它們的實(shí)現(xiàn)邏輯幾乎一樣,這里就其中一個(gè)為例子(代碼有刪減):
- (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
});
}
}
這里有幾個(gè)重要技術(shù)點(diǎn),很有意思。
釋放尾節(jié)點(diǎn)
通過(guò)一個(gè) while 循環(huán)不斷釋放尾節(jié)點(diǎn)removeTailNode,直到滿足參數(shù)ageLimit對(duì)時(shí)間的要求,而該鏈表的排序規(guī)則是:最近使用的內(nèi)存塊會(huì)移動(dòng)到鏈表頭部,也就保證了刪除的內(nèi)存永遠(yuǎn)是最不常使用的(后面會(huì)看到如何實(shí)現(xiàn)排序的)。
鎖的處理
不妨思考這樣一個(gè)問(wèn)題:為何要使用pthread_mutex_trylock()方法嘗試獲取鎖,而獲取失敗過(guò)后做了一個(gè)線程掛起操作usleep()?
優(yōu)先級(jí)反轉(zhuǎn):比如兩個(gè)線程 A 和 B,優(yōu)先級(jí) A < B。當(dāng) A 獲取鎖訪問(wèn)共享資源時(shí),B 嘗試獲取鎖,那么 B 就會(huì)進(jìn)入忙等狀態(tài),忙等時(shí)間越長(zhǎng)對(duì) CPU 資源的占用越大;而由于 A 的優(yōu)先級(jí)低于 B,A 無(wú)法與高優(yōu)先級(jí)的線程爭(zhēng)奪 CPU 資源,從而導(dǎo)致任務(wù)遲遲完成不了。解決優(yōu)先級(jí)反轉(zhuǎn)的方法有“優(yōu)先級(jí)天花板”和“優(yōu)先級(jí)繼承”,它們的核心操作都是提升當(dāng)前正在訪問(wèn)共享資源的線程的優(yōu)先級(jí)。
歷史情況:在老版本的代碼中,作者是使用的OSSpinLock自旋鎖來(lái)保證線程安全,而后來(lái)由于OSSpinLock的 bug 問(wèn)題(存在潛在的優(yōu)先級(jí)反轉(zhuǎn)BUG),作者將其替換成了pthread_mutex_t互斥鎖。
筆者的理解:
自動(dòng)的遞歸修剪邏輯是這樣的:
- (void)_trimInBackground {
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
而_queue是一個(gè)串行隊(duì)列:
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
可以明確的是,自動(dòng)修剪過(guò)程不存在線程安全問(wèn)題,當(dāng)然框架還暴露了修剪內(nèi)存的方法給外部使用,那么當(dāng)外部在多線程調(diào)用修剪內(nèi)存方法就可能會(huì)出現(xiàn)線程安全問(wèn)題。
這里做了一個(gè) 10ms 的掛起操作然后循環(huán)嘗試,直接舍棄了互斥鎖的空轉(zhuǎn)期,但這樣也避免了多線程訪問(wèn)下過(guò)多的空轉(zhuǎn)占用過(guò)多的 CPU 資源。作者這樣處理很可能加長(zhǎng)了修剪內(nèi)存的時(shí)間,但是卻避免了極限情況下空轉(zhuǎn)對(duì) CPU 的占用。
顯然,作者是期望使用者在后臺(tái)線程修剪內(nèi)存(最好使用者不去顯式的調(diào)用修剪內(nèi)存方法)。
異步線程釋放資源
這里作者使用了一個(gè)容器將要釋放的節(jié)點(diǎn)裝起來(lái),然后在某個(gè)隊(duì)列(默認(rèn)是非主隊(duì)列)里面調(diào)用了一下該容器的方法。雖然看代碼可能不理解,但是作者寫(xiě)了一句注釋release in queue:某個(gè)對(duì)象的方法最后在某個(gè)線程調(diào)用,這個(gè)對(duì)象就會(huì)在當(dāng)前線程釋放。很明顯,這里是作者將節(jié)點(diǎn)的釋放放其他線程,從而減輕主線程的資源開(kāi)銷(xiāo)。
(3)檢查內(nèi)存是否超限的定時(shí)任務(wù)
有這樣一段代碼:
- (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];
});
}
可以看到,作者是使用一個(gè)遞歸+延時(shí)來(lái)實(shí)現(xiàn)定時(shí)任務(wù)的,這里可以自定義檢測(cè)的時(shí)間間隔。
(4)進(jìn)入后臺(tái)和內(nèi)存警告的處理
在該類初始化時(shí),作者寫(xiě)了內(nèi)存警告和進(jìn)入后臺(tái)兩個(gè)監(jiān)聽(tīng):
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
然后可以由使用者定義在觸發(fā)響應(yīng)時(shí)是否需要清除內(nèi)存(簡(jiǎn)化了一下代碼):
- (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];
}
使用者還可以通過(guò)閉包實(shí)時(shí)監(jiān)聽(tīng)。
(5)讀數(shù)據(jù)
- (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;
}
邏輯很簡(jiǎn)單,關(guān)鍵的一步是 node->_time = CACurrentMediaTime() 和 [_lru bringNodeToHead:node] ;即更新這塊內(nèi)存的時(shí)間,然后將該節(jié)點(diǎn)移動(dòng)到鏈表頭部,實(shí)現(xiàn)了基于時(shí)間的優(yōu)先級(jí)排序,為 LRU 的實(shí)現(xiàn)提供了可靠的數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)。
(6)寫(xiě)數(shù)據(jù)
代碼有刪減,解析寫(xiě)在代碼中:
- (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的變量,將該節(jié)點(diǎn)移動(dòng)到頭部
......
[_lru bringNodeToHead:node];
} else {
//2 若緩存中沒(méi)有,創(chuàng)建一個(gè)內(nèi)存,將該節(jié)點(diǎn)插入到頭部
node = [_YYLinkedMapNode new];
......
[_lru insertNodeAtHead:node];
}
//3 判斷是否需要修剪內(nèi)存占用,若需要:異步修剪,保證寫(xiě)入的性能
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//4 判斷是否需要修剪內(nèi)存塊數(shù)量,若需要:默認(rèn)在非主隊(duì)列釋放無(wú)用內(nèi)存,保證寫(xiě)入的性能
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);
}
二、磁盤(pán)緩存:YYDiskCache
在暴露給用戶的 API 中,磁盤(pán)緩存的功能和內(nèi)存緩存很像,同樣有讀寫(xiě)數(shù)據(jù)和修剪數(shù)據(jù)等功能。
YYDiskCache的磁盤(pán)緩存處理性能非常優(yōu)越,作者測(cè)試了數(shù)據(jù)庫(kù)和文件存儲(chǔ)的讀寫(xiě)效率:iPhone 6 64G 下,SQLite 寫(xiě)入性能比直接寫(xiě)文件要高,但讀取性能取決于數(shù)據(jù)大小:當(dāng)單條數(shù)據(jù)小于 20K 時(shí),數(shù)據(jù)越小 SQLite 讀取性能越高;單條數(shù)據(jù)大于 20K 時(shí),直接寫(xiě)為文件速度會(huì)更快一些。(更詳細(xì)的說(shuō)明看文末鏈接)
所以作者對(duì)磁盤(pán)緩存的處理方式為 SQLite 結(jié)合文件存儲(chǔ)的方式。
磁盤(pán)緩存的核心類是YYKVStorage,注意該類是非線程安全的,它主要封裝了 SQLite 數(shù)據(jù)庫(kù)的操作和文件存儲(chǔ)操作。
后文的剖析大部分的代碼都是在YYKVStorage文件中。
(1)磁盤(pán)緩存的文件結(jié)構(gòu)
首先,需要了解一下作者設(shè)計(jì)的在磁盤(pán)中的文件結(jié)構(gòu)(在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 是一個(gè)初始化時(shí)使用的變量,不同的 path 對(duì)應(yīng)不同的數(shù)據(jù)庫(kù)。在 path 下面有 sqlite 數(shù)據(jù)庫(kù)相關(guān)的三個(gè)文件,以及兩個(gè)目錄(/data 和 /trash),這兩個(gè)目錄就是文件存儲(chǔ)方便直接讀取的地方,也就是為了實(shí)現(xiàn)上文說(shuō)的在高于某個(gè)臨界值時(shí)直接讀取文件比從數(shù)據(jù)庫(kù)讀取快的理論。
在數(shù)據(jù)庫(kù)中,建了一個(gè)表,表的結(jié)構(gòu)如上代碼所示:
- key 唯一標(biāo)識(shí)
- size 當(dāng)前內(nèi)存塊的大小。
- inline_data 使用者存儲(chǔ)內(nèi)容(value)的二進(jìn)制數(shù)據(jù)。
- last_access_time 最后訪問(wèn)時(shí)間,便于磁盤(pán)緩存實(shí)現(xiàn) LRU 算法的數(shù)據(jù)結(jié)構(gòu)排序。
- filename 文件名,它指向直接存文件情況下的文件名,具體交互請(qǐng)往下看~
如何實(shí)現(xiàn) SQLite 結(jié)合文件存儲(chǔ)
這一個(gè)重點(diǎn)問(wèn)題,就像之前說(shuō)的,在某個(gè)臨界值時(shí),直接讀取文件的效率要高于從數(shù)據(jù)庫(kù)讀取,第一反應(yīng)可能是寫(xiě)文件和寫(xiě)數(shù)據(jù)庫(kù)分離,也就是上面的結(jié)構(gòu)中,manifest.sqlite 數(shù)據(jù)庫(kù)文件和 /data 文件夾內(nèi)容無(wú)關(guān)聯(lián),讓 /data 去存儲(chǔ)高于臨界值的數(shù)據(jù),讓 sqlite 去存儲(chǔ)低于臨界值的數(shù)據(jù)。
然而這樣會(huì)帶來(lái)兩個(gè)問(wèn)題:
- /data 目錄下的緩存數(shù)據(jù)無(wú)法高速查找(可能只有遍歷)
- 無(wú)法統(tǒng)一管理磁盤(pán)緩存
為了完美處理該問(wèn)題,作者將它們結(jié)合了起來(lái),所有關(guān)于用戶存儲(chǔ)數(shù)據(jù)的相關(guān)信息都會(huì)放在數(shù)據(jù)庫(kù)中(即剛才說(shuō)的那個(gè)table中),而待存儲(chǔ)數(shù)據(jù)的二進(jìn)制文件,卻根據(jù)情況分別處理:要么存在數(shù)據(jù)庫(kù)表的 inline_data 下,要么直接存儲(chǔ)在 /data 文件夾下。
如此一來(lái),一切問(wèn)題迎刃而解,下文根據(jù)源碼進(jìn)行驗(yàn)證和探究。
(2)數(shù)據(jù)庫(kù)表的OC模型體現(xiàn)
當(dāng)然,為了讓接口可讀性更高,作者寫(xiě)了一個(gè)對(duì)應(yīng)數(shù)據(jù)庫(kù)表的模型,作為使用者實(shí)際業(yè)務(wù)使用的類:
@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
該類的屬性和數(shù)據(jù)庫(kù)表的鍵一一對(duì)應(yīng)。
(3)數(shù)據(jù)庫(kù)的操作封裝
對(duì)于 sqlite 的封裝比較常規(guī),作者的容錯(cuò)處理做得很好,下面就一些重點(diǎn)地方做一些講解,對(duì)數(shù)據(jù)庫(kù)操作感興趣的朋友可以直接去看源碼。
sqlite3_stmt 緩存
YYKVStorage 類有這樣一個(gè)變量:CFMutableDictionaryRef _dbStmtCache;
通過(guò) 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;
}
作者使用了一個(gè) hash 容器來(lái)緩存 stmt, 每次根據(jù) sql 生成 stmt 時(shí),若已經(jīng)存在緩存就執(zhí)行一次 sqlite3_reset(stmt); 讓 stmt 回到初始狀態(tài)。
如此一來(lái),提高了數(shù)據(jù)庫(kù)讀寫(xiě)的效率,是一個(gè)小 tip。
利用 sql 語(yǔ)句操作數(shù)據(jù)庫(kù)實(shí)現(xiàn) LRU
數(shù)據(jù)庫(kù)操作,仍然有根據(jù)占用內(nèi)存大小、最后訪問(wèn)時(shí)間、內(nèi)存塊數(shù)量進(jìn)行修剪內(nèi)存的方法,下面就根據(jù)最后訪問(wèn)時(shí)間進(jìn)行修剪方法做為例子:
- (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 語(yǔ)句,很輕松的實(shí)現(xiàn)了內(nèi)存的修剪。
寫(xiě)入時(shí)的核心邏輯
寫(xiě)入時(shí),作者根據(jù)是否有 filename 判斷是否需要將寫(xiě)入的數(shù)據(jù)二進(jìn)制存入數(shù)據(jù)庫(kù)(代碼有刪減):
- (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 ,雖然不會(huì)寫(xiě)入數(shù)據(jù)庫(kù),但是會(huì)直接寫(xiě)入 /data 文件夾,這個(gè)邏輯是在本類的 public 方法中做的。
(4)文件操作的封裝
主要是 NSFileManager 相關(guān)方法的基本使用,比較獨(dú)特的是,作者使用了一個(gè)“垃圾箱”,也就是磁盤(pán)文件存儲(chǔ)結(jié)構(gòu)中的 /trash 目錄。
可以看到兩個(gè)方法:
- (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];
}
});
}
上面?zhèn)€方法是將 /data 目錄下的文件移動(dòng)到 /trash 目錄下,下面?zhèn)€方法是將 /trash 目錄下的文件在異步線程清理掉。
筆者的理解:很容易想到,刪除文件是一個(gè)比較耗時(shí)的操作,所以作者把它放到了一個(gè)專門(mén)的隊(duì)列處理。而刪除的文件用一個(gè)專門(mén)的路徑 /trash 放置,避免了寫(xiě)入數(shù)據(jù)和刪除數(shù)據(jù)之間發(fā)生沖突。試想,若刪除的邏輯和寫(xiě)入的邏輯都是對(duì) /data 目錄進(jìn)行操作,而刪除邏輯比較耗時(shí),那么就會(huì)很容易出現(xiàn)誤刪等情況。
(5)YYDiskCache 對(duì) YYKVStorage 的二次封裝
對(duì)于 YYKVStorage 類的公有方法,筆者不做解析,就是對(duì)數(shù)據(jù)庫(kù)操作和寫(xiě)文件操作的一個(gè)結(jié)合封裝,很簡(jiǎn)單一看便知。
作者不提倡直接使用非線程安全的 YYKVStorage 類,所以封裝了一個(gè)線程安全的 YYDiskCache 類便于大家使用。
所以,YYDiskCache 類中主要是做了一些操作磁盤(pán)緩存的線程安全機(jī)制,是基于信號(hào)量(dispatch_semaphore)來(lái)處理的,暴露的接口中類似 YYMemoryCache 類的一系列方法。
剩余磁盤(pán)空間的限制
磁盤(pán)緩存中,多了一個(gè)如下修剪方法:
- (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];
}
根據(jù)剩余的磁盤(pán)空間的限制進(jìn)行修剪,作者確實(shí)想得很周到。_YYDiskSpaceFree()是作者寫(xiě)的一個(gè) c 方法,用于獲取剩余磁盤(pán)空間。
MD5 加密 key
- (NSString *)_filenameForKey:(NSString *)key {
NSString *filename = nil;
if (_customFileNameBlock) filename = _customFileNameBlock(key);
if (!filename) filename = _YYNSStringMD5(key);
return filename;
}
filename 是作者根據(jù)使用者傳入的 key 做一次 MD5 加密所得的字符串,所以不要誤以為文件名就是你傳入的 key (_YYNSStringMD5()是作者寫(xiě)的一個(gè)加密方法)。當(dāng)然,框架提供了一個(gè) _customFileNameBlock 允許你自定義文件名。
同時(shí)提供同步和異步接口
可以看到諸如此類的設(shè)計(jì):
- (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);
});
}
由于可能存儲(chǔ)的文件過(guò)大,在讀寫(xiě)時(shí)會(huì)占用過(guò)多的資源,所以作者對(duì)于這些操作都分別提供了同步和異步的接口,可謂非常人性化,這也是接口設(shè)計(jì)的一些值得學(xué)習(xí)的地方。
三、綜合封裝:YYCache
實(shí)際上上文的剖析已經(jīng)囊括了 YYCache 框架的核心了。YYCache 類主要是對(duì)內(nèi)存緩存和磁盤(pán)緩存的結(jié)合封裝,代碼很簡(jiǎn)單,有一點(diǎn)需要提出來(lái):
- (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);
}];
}
}
優(yōu)先查找內(nèi)存緩存_memoryCache中的數(shù)據(jù),若查不到,就查詢磁盤(pán)緩存_diskCache,查詢磁盤(pán)緩存成功,將數(shù)據(jù)同步到內(nèi)存緩存中,方便下次查找。
這么做的理由很簡(jiǎn)單:根據(jù)機(jī)械原理,較大的存儲(chǔ)設(shè)備要比較小的存儲(chǔ)設(shè)備運(yùn)行得慢,而快速設(shè)備的造價(jià)遠(yuǎn)高于低速設(shè)備。所以內(nèi)存緩存的讀寫(xiě)速度遠(yuǎn)高于磁盤(pán)緩存。這也是開(kāi)發(fā)中緩存設(shè)計(jì)的核心問(wèn)題,我們既要保證緩存讀寫(xiě)的效率,又要考慮到空間占用,其實(shí)又回到了空間和時(shí)間的權(quán)衡問(wèn)題了。
寫(xiě)在后面
YYCache 核心邏輯思路、接口設(shè)計(jì)、代碼組織架構(gòu)、容錯(cuò)處理、性能優(yōu)化、內(nèi)存管理、線程安全這些方面都做得很好很極致,閱讀起來(lái)非常舒服。
閱讀開(kāi)源框架,第一步一定是通讀一下 API 了解該框架是干什么的,然后采用“分治”的思路逐個(gè)擊破,類比“歸并算法”:先拆開(kāi)再合并,切勿想一口吃成胖子,特別是對(duì)于某些“重量級(jí)”框架。
希望讀者朋友們閱讀過(guò)后有所收獲??。
參考文獻(xiàn):作者 ibireme 的博客 YYCache 設(shè)計(jì)思路