有關(guān)iOS緩存的框架挺多的港华,有系統(tǒng)自帶的NSCache,或者一些三方的午衰,比如YYCahce,以及SDWebImage里的SDImageCache立宜。這些都是性能比較高的,代碼質(zhì)量也是比較高苇经,所以今天就把它們拿出來做個比較赘理。前面我對YYCache做了兩篇分析筆記,在研讀這篇文章之前大家先去閱讀一下扇单。
YYCache內(nèi)存緩存是用字典進(jìn)行的數(shù)據(jù)存儲商模,然后以雙向鏈表關(guān)聯(lián)起來一個邏輯結(jié)構(gòu)。YYCache的磁盤緩存是文件存儲和sqlite存儲的結(jié)合蜘澜。有關(guān)YYCache內(nèi)存緩存的細(xì)節(jié)分析施流,可以參考前面的幾篇文章。那兩篇文章主要分析了雙向鏈表是如何操作數(shù)據(jù)的鄙信。 有關(guān)磁盤緩存瞪醋,今天我想更深入的講解一下。
YYCache的磁盤緩存是用到了sqlite和文件的結(jié)合装诡,我們可以看下面的圖银受。
當(dāng)數(shù)據(jù)的大小大于20KB的時候,會給filename賦值鸦采,有了filename宾巍,數(shù)據(jù)就會以文件存儲的方式存儲,因?yàn)?0KB是一個臨界值渔伯,小于20KB以sqlite的方式存取數(shù)據(jù)比較快顶霞,大于20KB以文件的讀取方式存取數(shù)據(jù)比較快。這個大家自己可以去測試一下锣吼。雖然作者沒有說為什么是20KB选浑,但是我還是在sqlite官網(wǎng)找到了答案。如下圖:
關(guān)于文件存儲的操作這里就不說了玄叠,比較簡單古徒。這里重點(diǎn)講一下sqlite.
我們先看一段代碼
數(shù)據(jù)庫操作的主要流程可以這么看,這里有個查詢sql語句字符串读恃,為了能夠執(zhí)行隧膘,我們需要將這個sql字符串進(jìn)行處理崎苗,第二張圖就是對sql語句進(jìn)行的處理。主要方法是sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL)舀寓,它是將字符串轉(zhuǎn)化成可執(zhí)行的的字節(jié)流然后存儲在stmt這個對象里胆数,stmt可以理解成一個sqlite3_stmt類型的結(jié)構(gòu)體,也可以理解成是一個可執(zhí)行sql語句互墓。sqlite3_prepare_v2每次執(zhí)行的性能消耗是巨大的必尼。所以呢作者把sqlite3_stmt結(jié)構(gòu)體對象緩存起來,每次先從_dbStmtCache緩存里取數(shù)據(jù)篡撵。如果取到了之后重置一下判莉,然后直接返回,如果沒有取到那么才會執(zhí)行sqlite3_prepare_v2這個方法育谬。_dbPrepareStmt方法已經(jīng)處理好了sql語句券盅,也就是說可以執(zhí)行了。然后我們看源碼里看膛檀,這是一個查詢方法锰镀,所以需要先綁定key到stmt對象里,接著就是執(zhí)行sqlite3_step咖刃,這個方法是真正執(zhí)行sql語句的方法泳炉。
執(zhí)行完后返回每一行的數(shù)據(jù),我們?nèi)绻枰∶總€具體字段的數(shù)據(jù)嚎杨,就需要調(diào)用sqlite3_column_xx等方法花鹅。然后依次賦值到Y(jié)YKVStorageItem對象里,最后返回枫浙。完成了一個查詢操作刨肃,其他的插入,刪除箩帚,修改相關(guān)的操作也是類似真友。
除了sqlite常見的增刪改查相關(guān)的源碼,我還發(fā)現(xiàn)了作者也使用了sqlite的wal機(jī)制膏潮。這里也說一下吧锻狗。wal機(jī)制是sqlite3.7.0版本之后才有的满力,在sqlite3.7.0之前是rollback journal機(jī)制焕参。
rollback journal機(jī)制的原理是:在修改數(shù)據(jù)庫文件中的數(shù)據(jù)之前,先將修改所在分頁中的數(shù)據(jù)備份在另外一個地方油额,然后才將修改寫入到數(shù)據(jù)庫文件中叠纷;如果事務(wù)失敗,則將備份數(shù)據(jù)拷貝回來潦嘶,撤銷修改涩嚣;如果事務(wù)成功,則刪除備份數(shù)據(jù),提交修改航厚。( SQLite在3.7.0 之前)
WAL機(jī)制的原理是:修改并不直接寫入到數(shù)據(jù)庫文件中顷歌,而是寫入到另外一個稱為WAL的文件中;如果事務(wù)失敗幔睬,WAL中的記錄會被忽略眯漩,撤銷修改;如果事務(wù)成功麻顶,它將在隨后的某個時間被寫回到數(shù)據(jù)庫文件中赦抖,提交修改。( SQLite在3.7.0 之后)辅肾。如下圖:
同步WAL文件和數(shù)據(jù)庫文件的行為被稱為checkpoint(檢查點(diǎn))队萤,它由SQLite自動執(zhí)行,默認(rèn)是在WAL文件積累到1000頁修改的時候矫钓;當(dāng)然要尔,在適當(dāng)?shù)臅r候,也可以手動執(zhí)行checkpoint新娜,SQLite提供了相關(guān)的接口盈电。執(zhí)行checkpoint之后,WAL文件會被清空杯活。
我們可以看到作者是手動觸發(fā)的checkpoint方法匆帚,具體原因我猜測是為了提高存儲效率吧。
有關(guān)YYCache相關(guān)的內(nèi)容都說的差不多了旁钧。
除了YYCache,還有一個性能比較高的緩存框架吸重,那就是SDWebImage,這個大家都比較熟悉歪今,但是很少有人去分析它的緩存吧嚎幸。今天我們一起來看一下它是如何的與眾不同吧。
SD的緩存也分為內(nèi)存緩存和硬盤緩存寄猩,從類的聲明上可以看出SDMemoryCache繼承于NSCache嫉晶,SDMemoryCache具有NSCache所有的緩存API的功能,我們點(diǎn)進(jìn)去看看NSCache有哪些API,API都比較簡單田篇,有點(diǎn)像NSDictionary的API替废。存取都很方便。
SD為什么選擇使用 NSCache 來做內(nèi)存緩存
在 App 運(yùn)行過程中泊柬,我們通常會緩存一些短時間需要用到并且創(chuàng)建非常昂貴的對象椎镣;重用這些對象可以優(yōu)化性能,并且可以為用戶更快的展示數(shù)據(jù)兽赁。
一般是利用鍵值對這種數(shù)據(jù)格式來緩存所需的對象状答,在 Objective-C 中冷守,我們最常用的就是利用 NSDictionary & NSMutableDictionary 來保存鍵值對,那為什么內(nèi)存緩存不用 Dictionary 來實(shí)現(xiàn)呢惊科?我們可以思考下這些場景:
當(dāng)內(nèi)存緩存的對象越來越多拍摇,如何避免內(nèi)存暴增?如何有效的管理這些對象呢馆截?
遇到內(nèi)存警告的時候授翻,如何清除緩存對象,以便騰出空間給當(dāng)前需要內(nèi)存的應(yīng)用呢孙咪?是將緩存對象全部清除呢堪唐?還是只清除一部分緩存對象呢?
在多線程環(huán)境下翎蹈,調(diào)用 NSDictionary 的存取方法會不會出現(xiàn)問題呢淮菠?
NSDictionary 對于 Key-Value 有什么限制?
面對以上這些情況荤堪,官方推薦我們使用 NSCache 來實(shí)現(xiàn)內(nèi)存緩存『狭辏現(xiàn)在我們來看下 NSCache 的一些特性:
NSCache 具有自動刪減緩存策略(確保緩存不會占用太多內(nèi)存),所以我們不需要再去實(shí)現(xiàn)復(fù)雜的緩存淘汰算法
NSCache 的方法是線程安全的澄阳,不用我們再去考慮多線程安全問題(PS:NSDictionary 里面的存取方法不是線程安全的)
NSCache 對于 Key-Value 中的 Key 沒有要求(PS:NSDictionary 中要求 Key 必須實(shí)現(xiàn) NSCoping 協(xié)議)
NSCache 可以設(shè)置緩存中對象總個數(shù)和總成本拥知,這些尺度定義了緩存刪減其中對象的時機(jī)
綜上所訴,我們通常會使用 NSCache碎赢。
我們接著看源碼低剔,發(fā)現(xiàn)SDMemoryCache還有一個NSMapTable類型的緩存空間,這個緩存其實(shí)是個輔助的作用肮塞,可以稱為輔助緩存襟齿。我們一起來看下為什么會出現(xiàn)這種設(shè)計(jì)。
Create a subclass of NSCache using a weak cache. Only remove the cache when memory warning and sync back the alive instance from weak cache into cache.
這個是 SDWebImage 作者的提交日志
這個設(shè)計(jì)是針對下面這種場景進(jìn)行了優(yōu)化枕赵,不得不說很細(xì)節(jié):
首先 NSCache 會強(qiáng)引用緩存對象猜欺,然后我們的 NSCache 監(jiān)聽了內(nèi)存警告的通知,當(dāng)發(fā)生內(nèi)存警告的時候拷窜,NSCache 會 RemoveAllObjects开皿,移除所有緩存對象,以便騰出內(nèi)存空間處理當(dāng)下重要的任務(wù)篮昧。
前面說過赋荆,NSCache 會強(qiáng)引用緩存對象,在 NSCache 調(diào)用 SetObject 方法之后恋谭,會將對象的引用計(jì)數(shù)加 1糠睡,在 NSCache 調(diào)用了 RemoveAllObjects 方法之后挽鞠,就會將對象的引用計(jì)數(shù)減 1疚颊。
調(diào)用完 RemoveAllObjects 方法之后狈孔,這里就會有以下兩種情況:
1、該對象已經(jīng)沒有被其他對象所強(qiáng)引用了材义,此時均抽,這個對象的引用計(jì)數(shù)會為 0,對象會被完全的銷毀
2其掂、該對象還被其他對象所強(qiáng)引用油挥,在 NSCache 調(diào)用 RemoveAllObjects 方法將對象的引用計(jì)數(shù)減1之后,它的引用計(jì)數(shù)還是會大于0款熬,此時深寥,這個對象并不會被銷毀,但是這個對象卻被移除了緩存贤牛,實(shí)際上這個對象還是在內(nèi)存中
在面對第三點(diǎn)的第二種情況惋鹅,NSCache雖然移除了緩存對象,但是這個對象依然被其他對象強(qiáng)引用了殉簸,所以它并不會銷毀,還是存在在內(nèi)存中;換句話說佛南,就是下次我們可以重用該對象绷雏,并不需要重新創(chuàng)建。
所以面對這種情況蝠检,SDWebImage 會重新將該對象放回緩存中沐鼠,就沒有必要再去磁盤中查找照片。
SD內(nèi)存緩存相關(guān)的內(nèi)容都說完了叹谁,下面說說SD磁盤緩存相關(guān)的內(nèi)容迟杂。SD因?yàn)槭谴鎯Φ膱D片,圖片基本上都是大于20KB本慕,所以毋庸置疑排拷,作者直接使用了文件存儲的方式進(jìn)行了存儲。下面就是存儲的代碼锅尘。
有關(guān)文件存儲的相關(guān)操作這里就不說了监氢,這里想說一下SD磁盤緩存的清理策略是怎樣的。還是先看代碼吧藤违。
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// Compute content date key to be used for tests
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
cacheContentDateKey = NSURLContentAccessDateKey;
break;
case SDImageCacheConfigExpireTypeModificationDate:
cacheContentDateKey = NSURLContentModificationDateKey;
break;
default:
break;
}
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time or last access time (oldest first).
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
磁盤存儲方面SD是完全使用的是文件存儲浪腐。不過SD對文件存儲的操作也是非常6的。文件的存取都比較簡單顿乒,但是SD的文件清理方式也挺好议街。首先,通過磁盤文件路徑字符串生成一個路徑URL.根據(jù)配置的文件清理時間維度是訪問時間還是修改時間去文件路徑下進(jìn)行遍歷璧榄,這樣我們就拿到了所有的文件特漩,文件有很多屬性吧雹,生成的文件數(shù)組我們也可以自己去定制,比如這里設(shè)置了是否是文件目錄的key,時間維度是訪問時間還是修改時間涂身,第三個是文件大小雄卷。遍歷所有文件,取出每個文件的最后訪問時間蛤售,然后跟配置好的過期時間進(jìn)行對比丁鹉,將過期的文件存放在一個即將要被刪除的數(shù)組里面,后面集中進(jìn)行刪除操作悴能。沒有被刪除的文件也重新進(jìn)行文件總大小計(jì)算揣钦,目的是為了后面判斷再將過期文件都刪除之后,文件的總大小是否還是符合設(shè)定漠酿。如果文件大小依然大于我們之前的設(shè)定拂盯,那么再將剩余沒被刪除的數(shù)組進(jìn)行排序之后,依次刪除比較老被訪問的文件记靡,并且刪除之后進(jìn)行文件大小比對谈竿,直到最后文件總大小符合最先的設(shè)定。這樣文件的刪除操作就完成了摸吠。
總結(jié)一下:SD磁盤自動清理首先是刪除掉過期的所有文件空凸,然后判斷總文件大小是否符合預(yù)期,如果刪除之后總文件大小還是超出預(yù)定的最大大小寸痢,那么就對剩下的所有文件排序呀洲,排序之后進(jìn)行for循環(huán),逐個刪除文件啼止,直到達(dá)到預(yù)期大小道逗,完成清理工作。
現(xiàn)在我們比較一下YYCache献烦、SDImageCache滓窍、NSCache吧。
有關(guān)iOS高性能緩存源碼解析就分析到這里了巩那,有什么疑問可以和我交流吏夯,微博賬號:梅嘉慶