有效的本地 cache 機制,可以避免不必要的重復網絡加載,不僅能提高相關應用場景的資源加載速度,也可以避免不必要的流量浪費造成用戶損失。但是,由于緩存一般做法是通過 url 經過 md5 變換的值作為 key 進行存儲,因此對于同樣 url 的資源在第一次緩存之后如果沒有合適的清理機制,就會存在不同步的問題,導致 bug 的出現。本文就是再這樣的背景下,通過對比 NSURLCache、SDImageCache、YYCache、AFNetworking 等優秀開源庫,探索通用的 cache 自動清理方案。
NSURLCache 中的緩存清理方案
關于 NSURLCache,可以閱讀 這篇文章,其中詳細介紹了 url loading 系統中最關鍵的緩存部分。
緩存策略 NSURLRequestCachePolicy
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
NSURLRequestUseProtocolCachePolicy = 0,// 對特定的 URL 請求使用網絡協議中實現的緩存邏輯。默認策略
NSURLRequestReloadIgnoringLocalCacheData = 1,//數據需要從原始地址加載。不使用現有緩存
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // 不僅忽略本地緩存,同時也忽略代理服務器或其他中間介質目前已有的、協議允許的緩存(未實現)
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
NSURLRequestReturnCacheDataElseLoad = 2,// 無論緩存是否過期,先使用本地緩存數據。如果緩存中沒有請求所對應的數據,那么從原始地址加載數據。
NSURLRequestReturnCacheDataDontLoad = 3,// 無論緩存是否過期,先使用本地緩存數據。如果緩存中沒有請求所對應的數據,那么放棄從原始地址加載數據,請求視為失?。矗骸半x線”模式)
NSURLRequestReloadRevalidatingCacheData = 5, // 從原始地址確認緩存數據的合法性后,緩存數據就可以使用,否則從原始地址加載(未實現)
};
SDImageCache 中的緩存清理方案
SDImageCache 是優秀的第三方網絡圖片下載庫 SDWebImage 中使用的自定義 cache 類,也是該庫的重要組件之一。功能包括了常見的緩存存儲、查詢以及清除,通過閱讀 SDImageCache 的源碼,就可以很快找到 cache 自動清理的處理方案。
cache 組織結構
SDImageCache 由內存緩存和磁盤緩存兩部分組成,內存緩存使用 NSCache 實現,磁盤緩存通過NSFileManager的單例來管理,簡單易懂。日常開發中,當我們遇到 cache 相關的需求時,其實可以直接使用 SDImageCache 而不需要重新造輪子。
閱讀頭文件,有這樣唯一一個 designated 初始化方法:
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
由此可知,SDImageCache 的初始化需要指定一個磁盤目錄作為緩存的根目錄和一個文件存儲的目錄,通過命名空間機制就可以把不同應用場景的緩存目錄給隔離開,使用該機制最大的好處就在于,每一個自定的 cache 實例,包括該類的單例,都可以自動管理自己存儲路徑下的緩存文件,而不會對其他人的 cache 有所影響。
cache 清理機制
先說說內存緩存,由于 SDImageCache 使用 NSCache 來充當內存緩存,而
NSCache 本身就支持內存清理機制,當系統內存很低時該類的實例會自動釋放一些對象(模擬器除外),因此理論上不需要做什么額外的處理。該類有兩個可供開發者設置的屬性:
// 內存總消耗限制,If 0, there is no total cost limit. The default value is 0.
@property NSUInteger totalCostLimit;
// 內存總數量限制,If 0, there is no count limit. The default value is 0.
@property NSUInteger countLimit;
官方文檔也對這兩個屬性做了清晰的解釋,不過,SDImageCache 中采用的就是默認值,另外,在 NSCache 子類的初始化方法中監聽了系統內存警告的通知,當系統收到內存警告時,清空內存中所有的對象。內存清理 SDImageCache 就做了這么多。
下面說一下磁盤緩存。在 designated 初始化中,發現 cache 實例監聽了如下幾個通知:
[[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];
第一個通知應該有些多余,內存部分已經做了處理。仔細觀察處理后兩個通知的 selector 名大概能猜到,SDImageCache 在 app 將要被終止時和切后臺時都會做一次較舊文件的清除操作。
那么,什么樣的文件算是 oldFiles?
在 SDImageCache 的 designated 初始化方法中,有這樣一個被初始化的變量:
_config = [[SDImageCacheConfig alloc] init];
這個變量對應的是頭文件中只讀的屬性:
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
翻遍 SDImageCache.m 文件可以發現,這個屬性正是用于輔助處理磁盤緩存的清理操作的,如果外部沒有修改這個屬性,那么 SDImageCache 就會使用配置的默認值進行處理。
// 圖片下載完成后,是否解壓,默認 YES
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 是否禁用 iclould 備份,默認 YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
// 是否使用內存緩存,默認 YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
// 磁盤緩存文件的最大有效期,單位 second
@property (assign, nonatomic) NSInteger maxCacheAge;
// 最大緩存大小,單位 byte。默認0,不會基于緩存大小對磁盤緩存進行清理。
@property (assign, nonatomic) NSUInteger maxCacheSize;
具體如何清理,就要進入 deleteOldFiles 方法以及 backgroundDeleteOldFiles 方法中查看。閱讀源碼發現,最終都是調用這個方法:
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
根據時間有效性清理的步驟,有這樣一段注釋:
// 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.
根據緩存大小限制清理的步驟,有這樣一段注釋:
// 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.
在遍歷緩存目錄時,作者用到了 NSURL 的這個方法:
- (nullable NSDictionary<NSURLResourceKey, id> *)resourceValuesForKeys:(NSArray<NSURLResourceKey> *)keys error:(NSError **)error NS_AVAILABLE(10_6, 4_0);
針對每一個文件的 URL 鏈接,主要關注這幾個屬性:URL 是不是目錄的 path、URL 對應文件最近修改時間以及文件分配的存儲空間,即代碼中的:NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
這里給大家留個思考問題,作者在計算文件大小的時候,不使用 NSURLFileAllocatedSizeKey,而是用 NSURLTotalFileAllocatedSizeKey,為什么?
小結一下,SDImageCache 緩存時效性問題的處理方案是:內存緩存在遇到內存警告時全部清除,磁盤緩存根據外部配置在客戶端終止前或者切到后臺時進行清理,過期的緩存文件全部刪除,緩存總大小超過 maxSize時,從最大的文件開始刪除直到當前緩存大小在 maxSize/2 值以下。
YYCache 中的緩存清理方案
AFNetworking 中的緩存清理方案
AFNetworking 是專業的網絡請求框架,支持 NSURLConnection 和 NSURLSession 的請求方式,AFN 的下載緩存部分采用 NSURLCache,NSURLCache 是系統自動管理緩存部分,就不多做介紹,這里主要介紹下其為圖片請求設計的 cache:AFAutoPurgingImageCache。
cache 組織結構
AFAutoPurgingImageCache 只包括內存緩存部分,所以實現也比較簡單。內存緩存使用 NSMutableDictionary 實現。
cache 清理機制
默認最大內存緩存大小為 100M,每次內存超過最大值時,清理掉 60M 的空間。清除內存時,按照 LUR 算法,首先對內存中的圖片數據按照最近訪問時間進行排序,優先刪除最后訪問時間久遠的數據。
小結:網絡請求加載庫主要實現對 NSURLConnection 和 NSURLSession 的封裝,緩存部分主要還是使用系統的 NSURLCache 實現,重點關注下內存緩存清理時,如何像 AFNetworing 這樣采用 LRU 算法進行清理,提高 cache 的命中率。