iOS開發(fā)——cache自動(dòng)清理方案探索

有效的本地 cache 機(jī)制,可以避免不必要的重復(fù)網(wǎng)絡(luò)加載,不僅能提高相關(guān)應(yīng)用場(chǎng)景的資源加載速度,也可以避免不必要的流量浪費(fèi)造成用戶損失。但是,由于緩存一般做法是通過(guò) url 經(jīng)過(guò) md5 變換的值作為 key 進(jìn)行存儲(chǔ),因此對(duì)于同樣 url 的資源在第一次緩存之后如果沒有合適的清理機(jī)制,就會(huì)存在不同步的問(wèn)題,導(dǎo)致 bug 的出現(xiàn)。本文就是再這樣的背景下,通過(guò)對(duì)比 NSURLCache、SDImageCache、YYCache、AFNetworking 等優(yōu)秀開源庫(kù),探索通用的 cache 自動(dòng)清理方案。

NSURLCache 中的緩存清理方案

關(guān)于 NSURLCache,可以閱讀 這篇文章,其中詳細(xì)介紹了 url loading 系統(tǒng)中最關(guān)鍵的緩存部分。

緩存策略 NSURLRequestCachePolicy
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,// 對(duì)特定的 URL 請(qǐng)求使用網(wǎng)絡(luò)協(xié)議中實(shí)現(xiàn)的緩存邏輯。默認(rèn)策略
    NSURLRequestReloadIgnoringLocalCacheData = 1,//數(shù)據(jù)需要從原始地址加載。不使用現(xiàn)有緩存
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // 不僅忽略本地緩存,同時(shí)也忽略代理服務(wù)器或其他中間介質(zhì)目前已有的、協(xié)議允許的緩存(未實(shí)現(xiàn))
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
    NSURLRequestReturnCacheDataElseLoad = 2,// 無(wú)論緩存是否過(guò)期,先使用本地緩存數(shù)據(jù)。如果緩存中沒有請(qǐng)求所對(duì)應(yīng)的數(shù)據(jù),那么從原始地址加載數(shù)據(jù)。
    NSURLRequestReturnCacheDataDontLoad = 3,// 無(wú)論緩存是否過(guò)期,先使用本地緩存數(shù)據(jù)。如果緩存中沒有請(qǐng)求所對(duì)應(yīng)的數(shù)據(jù),那么放棄從原始地址加載數(shù)據(jù),請(qǐng)求視為失?。矗骸半x線”模式)
    NSURLRequestReloadRevalidatingCacheData = 5, // 從原始地址確認(rèn)緩存數(shù)據(jù)的合法性后,緩存數(shù)據(jù)就可以使用,否則從原始地址加載(未實(shí)現(xiàn))
};

SDImageCache 中的緩存清理方案

SDImageCache 是優(yōu)秀的第三方網(wǎng)絡(luò)圖片下載庫(kù) SDWebImage 中使用的自定義 cache 類,也是該庫(kù)的重要組件之一。功能包括了常見的緩存存儲(chǔ)、查詢以及清除,通過(guò)閱讀 SDImageCache 的源碼,就可以很快找到 cache 自動(dòng)清理的處理方案。

cache 組織結(jié)構(gòu)

SDImageCache 由內(nèi)存緩存和磁盤緩存兩部分組成,內(nèi)存緩存使用 NSCache 實(shí)現(xiàn),磁盤緩存通過(guò)NSFileManager的單例來(lái)管理,簡(jiǎn)單易懂。日常開發(fā)中,當(dāng)我們遇到 cache 相關(guān)的需求時(shí),其實(shí)可以直接使用 SDImageCache 而不需要重新造輪子。

閱讀頭文件,有這樣唯一一個(gè) designated 初始化方法:

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

由此可知,SDImageCache 的初始化需要指定一個(gè)磁盤目錄作為緩存的根目錄和一個(gè)文件存儲(chǔ)的目錄,通過(guò)命名空間機(jī)制就可以把不同應(yīng)用場(chǎng)景的緩存目錄給隔離開,使用該機(jī)制最大的好處就在于,每一個(gè)自定的 cache 實(shí)例,包括該類的單例,都可以自動(dòng)管理自己存儲(chǔ)路徑下的緩存文件,而不會(huì)對(duì)其他人的 cache 有所影響。

cache 清理機(jī)制

先說(shuō)說(shuō)內(nèi)存緩存,由于 SDImageCache 使用 NSCache 來(lái)充當(dāng)內(nèi)存緩存,而
NSCache 本身就支持內(nèi)存清理機(jī)制,當(dāng)系統(tǒng)內(nèi)存很低時(shí)該類的實(shí)例會(huì)自動(dòng)釋放一些對(duì)象(模擬器除外),因此理論上不需要做什么額外的處理。該類有兩個(gè)可供開發(fā)者設(shè)置的屬性:

// 內(nèi)存總消耗限制,If 0, there is no total cost limit. The default value is 0.
@property NSUInteger totalCostLimit;

// 內(nèi)存總數(shù)量限制,If 0, there is no count limit. The default value is 0.
@property NSUInteger countLimit;

官方文檔也對(duì)這兩個(gè)屬性做了清晰的解釋,不過(guò),SDImageCache 中采用的就是默認(rèn)值,另外,在 NSCache 子類的初始化方法中監(jiān)聽了系統(tǒng)內(nèi)存警告的通知,當(dāng)系統(tǒng)收到內(nèi)存警告時(shí),清空內(nèi)存中所有的對(duì)象。內(nèi)存清理 SDImageCache 就做了這么多。

下面說(shuō)一下磁盤緩存。在 designated 初始化中,發(fā)現(xiàn) cache 實(shí)例監(jiān)聽了如下幾個(gè)通知:

[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];

第一個(gè)通知應(yīng)該有些多余,內(nèi)存部分已經(jīng)做了處理。仔細(xì)觀察處理后兩個(gè)通知的 selector 名大概能猜到,SDImageCache 在 app 將要被終止時(shí)和切后臺(tái)時(shí)都會(huì)做一次較舊文件的清除操作。

那么,什么樣的文件算是 oldFiles?
在 SDImageCache 的 designated 初始化方法中,有這樣一個(gè)被初始化的變量:

_config = [[SDImageCacheConfig alloc] init];

這個(gè)變量對(duì)應(yīng)的是頭文件中只讀的屬性:

@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;

翻遍 SDImageCache.m 文件可以發(fā)現(xiàn),這個(gè)屬性正是用于輔助處理磁盤緩存的清理操作的,如果外部沒有修改這個(gè)屬性,那么 SDImageCache 就會(huì)使用配置的默認(rèn)值進(jìn)行處理。

// 圖片下載完成后,是否解壓,默認(rèn) YES
@property (assign, nonatomic) BOOL shouldDecompressImages;

// 是否禁用 iclould 備份,默認(rèn) YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

// 是否使用內(nèi)存緩存,默認(rèn) YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

// 磁盤緩存文件的最大有效期,單位 second
@property (assign, nonatomic) NSInteger maxCacheAge; 

// 最大緩存大小,單位 byte。默認(rèn)0,不會(huì)基于緩存大小對(duì)磁盤緩存進(jìn)行清理。
@property (assign, nonatomic) NSUInteger maxCacheSize;

具體如何清理,就要進(jìn)入 deleteOldFiles 方法以及 backgroundDeleteOldFiles 方法中查看。閱讀源碼發(fā)現(xiàn),最終都是調(diào)用這個(gè)方法:

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

根據(jù)時(shí)間有效性清理的步驟,有這樣一段注釋:

// 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.

根據(jù)緩存大小限制清理的步驟,有這樣一段注釋:

// If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
// Target half of our maximum cache size for this cleanup pass.

在遍歷緩存目錄時(shí),作者用到了 NSURL 的這個(gè)方法:

- (nullable NSDictionary<NSURLResourceKey, id> *)resourceValuesForKeys:(NSArray<NSURLResourceKey> *)keys error:(NSError **)error NS_AVAILABLE(10_6, 4_0);

針對(duì)每一個(gè)文件的 URL 鏈接,主要關(guān)注這幾個(gè)屬性:URL 是不是目錄的 path、URL 對(duì)應(yīng)文件最近修改時(shí)間以及文件分配的存儲(chǔ)空間,即代碼中的:NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
這里給大家留個(gè)思考問(wèn)題,作者在計(jì)算文件大小的時(shí)候,不使用 NSURLFileAllocatedSizeKey,而是用 NSURLTotalFileAllocatedSizeKey,為什么?

小結(jié)一下,SDImageCache 緩存時(shí)效性問(wèn)題的處理方案是:內(nèi)存緩存在遇到內(nèi)存警告時(shí)全部清除,磁盤緩存根據(jù)外部配置在客戶端終止前或者切到后臺(tái)時(shí)進(jìn)行清理,過(guò)期的緩存文件全部刪除,緩存總大小超過(guò) maxSize時(shí),從最大的文件開始刪除直到當(dāng)前緩存大小在 maxSize/2 值以下。

YYCache 中的緩存清理方案

AFNetworking 中的緩存清理方案

AFNetworking 是專業(yè)的網(wǎng)絡(luò)請(qǐng)求框架,支持 NSURLConnection 和 NSURLSession 的請(qǐng)求方式,AFN 的下載緩存部分采用 NSURLCache,NSURLCache 是系統(tǒng)自動(dòng)管理緩存部分,就不多做介紹,這里主要介紹下其為圖片請(qǐng)求設(shè)計(jì)的 cache:AFAutoPurgingImageCache。

cache 組織結(jié)構(gòu)

AFAutoPurgingImageCache 只包括內(nèi)存緩存部分,所以實(shí)現(xiàn)也比較簡(jiǎn)單。內(nèi)存緩存使用 NSMutableDictionary 實(shí)現(xiàn)。

cache 清理機(jī)制

默認(rèn)最大內(nèi)存緩存大小為 100M,每次內(nèi)存超過(guò)最大值時(shí),清理掉 60M 的空間。清除內(nèi)存時(shí),按照 LUR 算法,首先對(duì)內(nèi)存中的圖片數(shù)據(jù)按照最近訪問(wèn)時(shí)間進(jìn)行排序,優(yōu)先刪除最后訪問(wèn)時(shí)間久遠(yuǎn)的數(shù)據(jù)。

小結(jié):網(wǎng)絡(luò)請(qǐng)求加載庫(kù)主要實(shí)現(xiàn)對(duì) NSURLConnection 和 NSURLSession 的封裝,緩存部分主要還是使用系統(tǒng)的 NSURLCache 實(shí)現(xiàn),重點(diǎn)關(guān)注下內(nèi)存緩存清理時(shí),如何像 AFNetworing 這樣采用 LRU 算法進(jìn)行清理,提高 cache 的命中率。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容