iOS高性能緩存框架

有關(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é)合装诡,我們可以看下面的圖银受。


image.png
image.png

當(dāng)數(shù)據(jù)的大小大于20KB的時候,會給filename賦值鸦采,有了filename宾巍,數(shù)據(jù)就會以文件存儲的方式存儲,因?yàn)?0KB是一個臨界值渔伯,小于20KB以sqlite的方式存取數(shù)據(jù)比較快顶霞,大于20KB以文件的讀取方式存取數(shù)據(jù)比較快。這個大家自己可以去測試一下锣吼。雖然作者沒有說為什么是20KB选浑,但是我還是在sqlite官網(wǎng)找到了答案。如下圖:


sqlite@2x.png

關(guān)于文件存儲的操作這里就不說了玄叠,比較簡單古徒。這里重點(diǎn)講一下sqlite.

我們先看一段代碼


image.png

image.png

數(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語句的方法泳炉。


image.png

執(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@2x.png

同步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文件會被清空杯活。

image.png

我們可以看到作者是手動觸發(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)行了存儲。下面就是存儲的代碼锅尘。

image.png

有關(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吧。


image.png
image.png

有關(guān)iOS高性能緩存源碼解析就分析到這里了巩那,有什么疑問可以和我交流吏夯,微博賬號:梅嘉慶

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市即横,隨后出現(xiàn)的幾起案子噪生,更是在濱河造成了極大的恐慌,老刑警劉巖东囚,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跺嗽,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)桨嫁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門植兰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞧甩,你說我怎么就攤上這事钉跷∶逐校” “怎么了肚逸?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長彬坏。 經(jīng)常有香客問我朦促,道長,這世上最難降的妖魔是什么栓始? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任务冕,我火速辦了婚禮,結(jié)果婚禮上幻赚,老公的妹妹穿的比我還像新娘禀忆。我一直安慰自己,他們只是感情好落恼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布箩退。 她就那樣靜靜地躺著,像睡著了一般佳谦。 火紅的嫁衣襯著肌膚如雪戴涝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天钻蔑,我揣著相機(jī)與錄音啥刻,去河邊找鬼。 笑死咪笑,一個胖子當(dāng)著我的面吹牛可帽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窗怒,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蘑拯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了兜粘?” 一聲冷哼從身側(cè)響起申窘,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎孔轴,沒想到半個月后剃法,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡路鹰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年贷洲,在試婚紗的時候發(fā)現(xiàn)自己被綠了收厨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡优构,死狀恐怖诵叁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钦椭,我是刑警寧澤拧额,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站彪腔,受9級特大地震影響侥锦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜德挣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一恭垦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧格嗅,春花似錦番挺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至懂扼,卻和暖如春禁荸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阀湿。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工赶熟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陷嘴。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓映砖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灾挨。 傳聞我的和親對象是個殘疾皇子邑退,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容