系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點(diǎn)
YYModel 源碼剖析:關(guān)注性能
YYImage 源碼剖析:圖片處理技巧
YYWebImage 源碼剖析:線程處理與緩存策略
寫在前面
YYCache 作為當(dāng)下 iOS 圈最流行的緩存框架,有著優(yōu)越的性能和絕佳的設(shè)計(jì)。筆者花了些時(shí)間對其“解剖”了一番翁逞,發(fā)現(xiàn)了很多有意思的東西叙赚,所以寫下本文分享一下痒玩。
考慮到篇幅谨胞,筆者對于源碼的解析不會過多的涉及 API 使用和一些基礎(chǔ)知識,更多的是剖析作者 ibireme 的設(shè)計(jì)思維和重要技術(shù)實(shí)現(xiàn)細(xì)節(jié)凝颇。
YYCache 主要分為兩部分:內(nèi)存緩存和磁盤緩存(對應(yīng) YYMemoryCache
和 YYDiskCache
)筐钟。在日常開發(fā)業(yè)務(wù)使用中揩瞪,多是直接操作 YYCache 類,該類是對內(nèi)存緩存功能和磁盤緩存功能的一個(gè)簡單封裝篓冲。
源碼基于 1.0.4 版本李破。
一、內(nèi)存緩存:YYMemoryCache
總覽 API 壹将,會發(fā)現(xià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;
......
可以看出嗤攻,該類主要包含讀寫功能和修剪功能(修剪是為了控制內(nèi)存緩存的大小等)。當(dāng)然诽俯,還有其他一些自定義方法妇菱,比如釋放操作的線程選擇、內(nèi)存警告和進(jìn)入后臺時(shí)是否清除內(nèi)存緩存等暴区。
對該類的基本功能有了了解之后闯团,就可以直接切實(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)該一眼就看出來貓膩,作者是使用的一個(gè)雙向鏈表+散列容器來實(shí)現(xiàn) LRU 的隔心。
鏈表的節(jié)點(diǎn) (_YYLinkedMapNode):
- 同時(shí)使用前驅(qū)和后繼指針(即
_prev
和_next
)是為了快速找到前驅(qū)和后繼節(jié)點(diǎn)白群。 - 這里使用
__unsafe_unretained
而不使用__weak
。雖然兩者都不會持有指針?biāo)赶虻膶ο蠹醚祝窃谥赶驅(qū)ο筢尫艜r(shí),前者并不會自動置空指針辐真,形成野指針须尚,不過經(jīng)過筆者后面的閱讀崖堤,發(fā)現(xiàn)作者避免了野指針的出現(xiàn);而且從性能層面看(作者原話):訪問具有 __weak 屬性的變量時(shí)耐床,實(shí)際上會調(diào)用objc_loadWeak()
和objc_storeWeak()
來完成密幔,這也會帶來很大的開銷,所以要避免使用__weak
屬性撩轰。 -
_key
和_value
就是框架使用者想要存儲的鍵值對胯甩,可以看出作者的設(shè)計(jì)是一個(gè)鍵值對對應(yīng)一個(gè)節(jié)點(diǎn)(_YYLinkedMapNode
)。 -
_cost
和_time
表示該節(jié)點(diǎn)的內(nèi)存大小和最后訪問的時(shí)間堪嫂。
LRU 實(shí)現(xiàn)類 (_YYLinkedMap) :
- 包含頭尾指針(
_head
和_tail
)偎箫,保證雙端查詢的效率。 -
_totalCost
和_totalCount
記錄最大內(nèi)存占用限制和數(shù)量限制皆串。 -
_releaseOnMainThread
和_releaseAsynchronously
分別表示在主線程釋放和在異步線程釋放淹办,它們的實(shí)現(xiàn)后文會講到。 -
_dic
變量是 OC 開發(fā)中常用的散列容器恶复,所有節(jié)點(diǎn)都會在_dic
中以 key-value 的形式存在怜森,保證常數(shù)級查詢效率。
既然是 LRU 算法谤牡,怎么能只有數(shù)據(jù)結(jié)構(gòu)副硅,往下面看 _YYLinkedMap 類實(shí)現(xiàn)了如下算法(嗯,挺常規(guī)的節(jié)點(diǎn)操作翅萤,代碼質(zhì)量挺高的恐疲,就不說明實(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)存的邏輯
正如一開始貼的 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)
通過一個(gè) while 循環(huán)不斷釋放尾節(jié)點(diǎn)removeTailNode
,直到滿足參數(shù)ageLimit
對時(shí)間的要求阵苇,而該鏈表的排序規(guī)則是:最近使用的內(nèi)存塊會移動到鏈表頭部壁公,也就保證了刪除的內(nèi)存永遠(yuǎn)是最不常使用的(后面會看到如何實(shí)現(xiàn)排序的)。
鎖的處理
不妨思考這樣一個(gè)問題:為何要使用pthread_mutex_trylock()
方法嘗試獲取鎖绅项,而獲取失敗過后做了一個(gè)線程掛起操作usleep()
紊册?
優(yōu)先級反轉(zhuǎn):比如兩個(gè)線程 A 和 B,優(yōu)先級 A < B快耿。當(dāng) A 獲取鎖訪問共享資源時(shí)囊陡,B 嘗試獲取鎖芳绩,那么 B 就會進(jìn)入忙等狀態(tài),忙等時(shí)間越長對 CPU 資源的占用越大撞反;而由于 A 的優(yōu)先級低于 B妥色,A 無法與高優(yōu)先級的線程爭奪 CPU 資源,從而導(dǎo)致任務(wù)遲遲完成不了遏片。解決優(yōu)先級反轉(zhuǎn)的方法有“優(yōu)先級天花板”和“優(yōu)先級繼承”嘹害,它們的核心操作都是提升當(dāng)前正在訪問共享資源的線程的優(yōu)先級。
歷史情況:在老版本的代碼中吮便,作者是使用的OSSpinLock
自旋鎖來保證線程安全笔呀,而后來由于OSSpinLock
的 bug 問題(存在潛在的優(yōu)先級反轉(zhuǎn)BUG),作者將其替換成了pthread_mutex_t
互斥鎖线衫。
筆者的理解:
自動的遞歸修剪邏輯是這樣的:
- (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)然框架還暴露了修剪內(nèi)存的方法給外部使用授账,那么當(dāng)外部在多線程調(diào)用修剪內(nèi)存方法就可能會出現(xiàn)線程安全問題枯跑。
這里做了一個(gè) 10ms 的掛起操作然后循環(huán)嘗試,直接舍棄了互斥鎖的空轉(zhuǎn)期白热,但這樣也避免了多線程訪問下過多的空轉(zhuǎn)占用過多的 CPU 資源敛助。作者這樣處理很可能加長了修剪內(nèi)存的時(shí)間,但是卻避免了極限情況下空轉(zhuǎn)對 CPU 的占用屋确。
顯然纳击,作者是期望使用者在后臺線程修剪內(nèi)存(最好使用者不去顯式的調(diào)用修剪內(nèi)存方法)。
異步線程釋放資源
這里作者使用了一個(gè)容器將要釋放的節(jié)點(diǎn)裝起來攻臀,然后在某個(gè)隊(duì)列(默認(rèn)是非主隊(duì)列)里面調(diào)用了一下該容器的方法焕数。雖然看代碼可能不理解,但是作者寫了一句注釋release in queue
:某個(gè)對象的方法最后在某個(gè)線程調(diào)用刨啸,這個(gè)對象就會在當(dāng)前線程釋放堡赔。很明顯,這里是作者將節(jié)點(diǎn)的釋放放其他線程设联,從而減輕主線程的資源開銷善已。
(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í)來實(shí)現(xiàn)定時(shí)任務(wù)的离例,這里可以自定義檢測的時(shí)間間隔换团。
(4)進(jìn)入后臺和內(nèi)存警告的處理
在該類初始化時(shí),作者寫了內(nèi)存警告和進(jìn)入后臺兩個(gè)監(jiān)聽:
[[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)存(簡化了一下代碼):
- (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];
}
使用者還可以通過閉包實(shí)時(shí)監(jiān)聽宫蛆。
(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;
}
邏輯很簡單艘包,關(guān)鍵的一步是 node->_time = CACurrentMediaTime()
和 [_lru bringNodeToHead:node]
;即更新這塊內(nèi)存的時(shí)間,然后將該節(jié)點(diǎn)移動到鏈表頭部想虎,實(shí)現(xiàn)了基于時(shí)間的優(yōu)先級排序衰絮,為 LRU 的實(shí)現(xiàn)提供了可靠的數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)。
(6)寫數(shù)據(jù)
代碼有刪減磷醋,解析寫在代碼中:
- (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)移動到頭部
......
[_lru bringNodeToHead:node];
} else {
//2 若緩存中沒有胡诗,創(chuàng)建一個(gè)內(nèi)存邓线,將該節(jié)點(diǎn)插入到頭部
node = [_YYLinkedMapNode new];
......
[_lru insertNodeAtHead:node];
}
//3 判斷是否需要修剪內(nèi)存占用,若需要:異步修剪煌恢,保證寫入的性能
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//4 判斷是否需要修剪內(nèi)存塊數(shù)量骇陈,若需要:默認(rèn)在非主隊(duì)列釋放無用內(nèi)存,保證寫入的性能
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);
}
二瑰抵、磁盤緩存:YYDiskCache
在暴露給用戶的 API 中你雌,磁盤緩存的功能和內(nèi)存緩存很像,同樣有讀寫數(shù)據(jù)和修剪數(shù)據(jù)等功能二汛。
YYDiskCache
的磁盤緩存處理性能非常優(yōu)越婿崭,作者測試了數(shù)據(jù)庫和文件存儲的讀寫效率:iPhone 6 64G 下,SQLite 寫入性能比直接寫文件要高肴颊,但讀取性能取決于數(shù)據(jù)大忻フ弧:當(dāng)單條數(shù)據(jù)小于 20K 時(shí),數(shù)據(jù)越小 SQLite 讀取性能越高婿着;單條數(shù)據(jù)大于 20K 時(shí)授瘦,直接寫為文件速度會更快一些。(更詳細(xì)的說明看文末鏈接)
所以作者對磁盤緩存的處理方式為 SQLite 結(jié)合文件存儲的方式竟宋。
磁盤緩存的核心類是YYKVStorage
提完,注意該類是非線程安全的,它主要封裝了 SQLite 數(shù)據(jù)庫的操作和文件存儲操作丘侠。
后文的剖析大部分的代碼都是在YYKVStorage
文件中徒欣。
(1)磁盤緩存的文件結(jié)構(gòu)
首先,需要了解一下作者設(shè)計(jì)的在磁盤中的文件結(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 對應(yīng)不同的數(shù)據(jù)庫帚称。在 path 下面有 sqlite 數(shù)據(jù)庫相關(guān)的三個(gè)文件,以及兩個(gè)目錄(/data 和 /trash)秽澳,這兩個(gè)目錄就是文件存儲方便直接讀取的地方闯睹,也就是為了實(shí)現(xiàn)上文說的在高于某個(gè)臨界值時(shí)直接讀取文件比從數(shù)據(jù)庫讀取快的理論。
在數(shù)據(jù)庫中担神,建了一個(gè)表楼吃,表的結(jié)構(gòu)如上代碼所示:
- key 唯一標(biāo)識
- size 當(dāng)前內(nèi)存塊的大小。
- inline_data 使用者存儲內(nèi)容(value)的二進(jìn)制數(shù)據(jù)。
- last_access_time 最后訪問時(shí)間孩锡,便于磁盤緩存實(shí)現(xiàn) LRU 算法的數(shù)據(jù)結(jié)構(gòu)排序酷宵。
- filename 文件名,它指向直接存文件情況下的文件名躬窜,具體交互請往下看~
如何實(shí)現(xiàn) SQLite 結(jié)合文件存儲
這一個(gè)重點(diǎn)問題浇垦,就像之前說的,在某個(gè)臨界值時(shí)荣挨,直接讀取文件的效率要高于從數(shù)據(jù)庫讀取男韧,第一反應(yīng)可能是寫文件和寫數(shù)據(jù)庫分離,也就是上面的結(jié)構(gòu)中默垄,manifest.sqlite 數(shù)據(jù)庫文件和 /data 文件夾內(nèi)容無關(guān)聯(lián)此虑,讓 /data 去存儲高于臨界值的數(shù)據(jù),讓 sqlite 去存儲低于臨界值的數(shù)據(jù)口锭。
然而這樣會帶來兩個(gè)問題:
- /data 目錄下的緩存數(shù)據(jù)無法高速查找(可能只有遍歷)
- 無法統(tǒng)一管理磁盤緩存
為了完美處理該問題朦前,作者將它們結(jié)合了起來,所有關(guān)于用戶存儲數(shù)據(jù)的相關(guān)信息都會放在數(shù)據(jù)庫中(即剛才說的那個(gè)table中)鹃操,而待存儲數(shù)據(jù)的二進(jìn)制文件韭寸,卻根據(jù)情況分別處理:要么存在數(shù)據(jù)庫表的 inline_data 下,要么直接存儲在 /data 文件夾下荆隘。
如此一來棒仍,一切問題迎刃而解,下文根據(jù)源碼進(jìn)行驗(yàn)證和探究臭胜。
(2)數(shù)據(jù)庫表的OC模型體現(xiàn)
當(dāng)然莫其,為了讓接口可讀性更高,作者寫了一個(gè)對應(yīng)數(shù)據(jù)庫表的模型耸三,作為使用者實(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ù)庫表的鍵一一對應(yīng)乱陡。
(3)數(shù)據(jù)庫的操作封裝
對于 sqlite 的封裝比較常規(guī),作者的容錯(cuò)處理做得很好仪壮,下面就一些重點(diǎn)地方做一些講解憨颠,對數(shù)據(jù)庫操作感興趣的朋友可以直接去看源碼。
sqlite3_stmt 緩存
YYKVStorage 類有這樣一個(gè)變量: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;
}
作者使用了一個(gè) hash 容器來緩存 stmt积锅, 每次根據(jù) sql 生成 stmt 時(shí)爽彤,若已經(jīng)存在緩存就執(zhí)行一次 sqlite3_reset(stmt);
讓 stmt 回到初始狀態(tài)。
如此一來缚陷,提高了數(shù)據(jù)庫讀寫的效率适篙,是一個(gè)小 tip。
利用 sql 語句操作數(shù)據(jù)庫實(shí)現(xiàn) LRU
數(shù)據(jù)庫操作箫爷,仍然有根據(jù)占用內(nèi)存大小嚷节、最后訪問時(shí)間聂儒、內(nèi)存塊數(shù)量進(jìn)行修剪內(nèi)存的方法,下面就根據(jù)最后訪問時(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 語句衩婚,很輕松的實(shí)現(xiàn)了內(nèi)存的修剪。
寫入時(shí)的核心邏輯
寫入時(shí)效斑,作者根據(jù)是否有 filename 判斷是否需要將寫入的數(shù)據(jù)二進(jìn)制存入數(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;
......
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 非春,雖然不會寫入數(shù)據(jù)庫,但是會直接寫入 /data 文件夾缓屠,這個(gè)邏輯是在本類的 public 方法中做的税娜。
(4)文件操作的封裝
主要是 NSFileManager 相關(guān)方法的基本使用,比較獨(dú)特的是藏研,作者使用了一個(gè)“垃圾箱”,也就是磁盤文件存儲結(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 目錄下的文件移動到 /trash 目錄下蠢挡,下面?zhèn)€方法是將 /trash 目錄下的文件在異步線程清理掉。
筆者的理解:很容易想到凳忙,刪除文件是一個(gè)比較耗時(shí)的操作业踏,所以作者把它放到了一個(gè)專門的隊(duì)列處理。而刪除的文件用一個(gè)專門的路徑 /trash 放置涧卵,避免了寫入數(shù)據(jù)和刪除數(shù)據(jù)之間發(fā)生沖突勤家。試想,若刪除的邏輯和寫入的邏輯都是對 /data 目錄進(jìn)行操作柳恐,而刪除邏輯比較耗時(shí)伐脖,那么就會很容易出現(xiàn)誤刪等情況。
(5)YYDiskCache 對 YYKVStorage 的二次封裝
對于 YYKVStorage 類的公有方法乐设,筆者不做解析讼庇,就是對數(shù)據(jù)庫操作和寫文件操作的一個(gè)結(jié)合封裝,很簡單一看便知近尚。
作者不提倡直接使用非線程安全的 YYKVStorage 類蠕啄,所以封裝了一個(gè)線程安全的 YYDiskCache 類便于大家使用。
所以戈锻,YYDiskCache 類中主要是做了一些操作磁盤緩存的線程安全機(jī)制歼跟,是基于信號量(dispatch_semaphore
)來處理的,暴露的接口中類似 YYMemoryCache 類的一系列方法格遭。
剩余磁盤空間的限制
磁盤緩存中哈街,多了一個(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ù)剩余的磁盤空間的限制進(jìn)行修剪,作者確實(shí)想得很周到拒迅。_YYDiskSpaceFree()
是作者寫的一個(gè) c 方法叹卷,用于獲取剩余磁盤空間撼港。
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()
是作者寫的一個(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);
});
}
由于可能存儲的文件過大蒙揣,在讀寫時(shí)會占用過多的資源靶溜,所以作者對于這些操作都分別提供了同步和異步的接口,可謂非常人性化懒震,這也是接口設(shè)計(jì)的一些值得學(xué)習(xí)的地方罩息。
三、綜合封裝:YYCache
實(shí)際上上文的剖析已經(jīng)囊括了 YYCache 框架的核心了个扰。YYCache 類主要是對內(nèi)存緩存和磁盤緩存的結(jié)合封裝瓷炮,代碼很簡單,有一點(diǎn)需要提出來:
- (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ù)递宅,若查不到娘香,就查詢磁盤緩存_diskCache
,查詢磁盤緩存成功办龄,將數(shù)據(jù)同步到內(nèi)存緩存中逗旁,方便下次查找朵你。
這么做的理由很簡單:根據(jù)機(jī)械原理抹缕,較大的存儲設(shè)備要比較小的存儲設(shè)備運(yùn)行得慢俊嗽,而快速設(shè)備的造價(jià)遠(yuǎn)高于低速設(shè)備。所以內(nèi)存緩存的讀寫速度遠(yuǎn)高于磁盤緩存英融。這也是開發(fā)中緩存設(shè)計(jì)的核心問題盏檐,我們既要保證緩存讀寫的效率,又要考慮到空間占用驶悟,其實(shí)又回到了空間和時(shí)間的權(quán)衡問題了糯笙。
寫在后面
YYCache 核心邏輯思路、接口設(shè)計(jì)撩银、代碼組織架構(gòu)给涕、容錯(cuò)處理、性能優(yōu)化额获、內(nèi)存管理够庙、線程安全這些方面都做得很好很極致,閱讀起來非常舒服抄邀。
閱讀開源框架耘眨,第一步一定是通讀一下 API 了解該框架是干什么的,然后采用“分治”的思路逐個(gè)擊破境肾,類比“歸并算法”:先拆開再合并剔难,切勿想一口吃成胖子胆屿,特別是對于某些“重量級”框架。
希望讀者朋友們閱讀過后有所收獲??偶宫。
參考文獻(xiàn):作者 ibireme 的博客 YYCache 設(shè)計(jì)思路