有效的本地 cache 機(jī)制,可以避免不必要的重復(fù)網(wǎng)絡(luò)加載,不僅能提高相關(guān)應(yīng)用場(chǎng)景的資源加載速度,也可以避免不必要的流量浪費(fèi)造成用戶損失。但是,由于緩存一般做法是通過 url 經(jīng)過 md5 變換的值作為 key 進(jìn)行存儲(chǔ),因此對(duì)于同樣 url 的資源在第一次緩存之后如果沒有合適的清理機(jī)制,就會(huì)存在不同步的問題,導(dǎo)致 bug 的出現(xiàn)。本文就是再這樣的背景下,通過對(duì)比 NSURLCache、SDImageCache、YYCache、AFNetworking 等優(yōu)秀開源庫,探索通用的 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,// 無論緩存是否過期,先使用本地緩存數(shù)據(jù)。如果緩存中沒有請(qǐng)求所對(duì)應(yīng)的數(shù)據(jù),那么從原始地址加載數(shù)據(jù)。
NSURLRequestReturnCacheDataDontLoad = 3,// 無論緩存是否過期,先使用本地緩存數(shù)據(jù)。如果緩存中沒有請(qǐng)求所對(duì)應(yīng)的數(shù)據(jù),那么放棄從原始地址加載數(shù)據(jù),請(qǐng)求視為失敗(即:“離線”模式)
NSURLRequestReloadRevalidatingCacheData = 5, // 從原始地址確認(rèn)緩存數(shù)據(jù)的合法性后,緩存數(shù)據(jù)就可以使用,否則從原始地址加載(未實(shí)現(xiàn))
};
SDImageCache 中的緩存清理方案
SDImageCache 是優(yōu)秀的第三方網(wǎng)絡(luò)圖片下載庫 SDWebImage 中使用的自定義 cache 類,也是該庫的重要組件之一。功能包括了常見的緩存存儲(chǔ)、查詢以及清除,通過閱讀 SDImageCache 的源碼,就可以很快找到 cache 自動(dòng)清理的處理方案。
cache 組織結(jié)構(gòu)
SDImageCache 由內(nèi)存緩存和磁盤緩存兩部分組成,內(nèi)存緩存使用 NSCache 實(shí)現(xiàn),磁盤緩存通過NSFileManager的單例來管理,簡(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ǔ)的目錄,通過命名空間機(jī)制就可以把不同應(yīng)用場(chǎng)景的緩存目錄給隔離開,使用該機(jī)制最大的好處就在于,每一個(gè)自定的 cache 實(shí)例,包括該類的單例,都可以自動(dòng)管理自己存儲(chǔ)路徑下的緩存文件,而不會(huì)對(duì)其他人的 cache 有所影響。
cache 清理機(jī)制
先說說內(nèi)存緩存,由于 SDImageCache 使用 NSCache 來充當(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è)屬性做了清晰的解釋,不過,SDImageCache 中采用的就是默認(rèn)值,另外,在 NSCache 子類的初始化方法中監(jiān)聽了系統(tǒng)內(nèi)存警告的通知,當(dāng)系統(tǒng)收到內(nèi)存警告時(shí),清空內(nèi)存中所有的對(duì)象。內(nèi)存清理 SDImageCache 就做了這么多。
下面說一下磁盤緩存。在 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è)思考問題,作者在計(jì)算文件大小的時(shí)候,不使用 NSURLFileAllocatedSizeKey,而是用 NSURLTotalFileAllocatedSizeKey,為什么?
小結(jié)一下,SDImageCache 緩存時(shí)效性問題的處理方案是:內(nèi)存緩存在遇到內(nèi)存警告時(shí)全部清除,磁盤緩存根據(jù)外部配置在客戶端終止前或者切到后臺(tái)時(shí)進(jìn)行清理,過期的緩存文件全部刪除,緩存總大小超過 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)存超過最大值時(shí),清理掉 60M 的空間。清除內(nèi)存時(shí),按照 LUR 算法,首先對(duì)內(nèi)存中的圖片數(shù)據(jù)按照最近訪問時(shí)間進(jìn)行排序,優(yōu)先刪除最后訪問時(shí)間久遠(yuǎn)的數(shù)據(jù)。
小結(jié):網(wǎng)絡(luò)請(qǐng)求加載庫主要實(shí)現(xiàn)對(duì) NSURLConnection 和 NSURLSession 的封裝,緩存部分主要還是使用系統(tǒng)的 NSURLCache 實(shí)現(xiàn),重點(diǎn)關(guān)注下內(nèi)存緩存清理時(shí),如何像 AFNetworing 這樣采用 LRU 算法進(jìn)行清理,提高 cache 的命中率。