Untold numbers of developers have hacked together an awkward, fragile system for network caching functionality, all because they weren’t aware that NSURLCache could be setup in two lines and do it 100× better. Even more developers have never known the benefits of network caching, and never attempted a solution, causing their apps to make untold numbers of unnecessary requests to the server.
無(wú)數(shù)開(kāi)發(fā)者嘗試自己做一個(gè)丑陋而脆弱的系統(tǒng)來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)緩存的功能迟赃,殊不知NSURLCache只要兩行代碼就能搞定,并且好上100倍麦轰。甚至更多的開(kāi)發(fā)者根本不知道網(wǎng)絡(luò)緩存的好處雨膨,從來(lái)沒(méi)有嘗試過(guò)解決方案但骨,導(dǎo)致他們的App向服務(wù)器發(fā)出無(wú)數(shù)不必要的請(qǐng)求蜘犁。
iOS系統(tǒng)的緩存策略
????上面是引用Mattt
大神在NSHipster介紹NSURLCache時(shí)的原話姚炕。
服務(wù)端的緩存策略
????先看看服務(wù)端的緩存策略征唬。當(dāng)?shù)谝淮握?qǐng)求后菩咨,客戶端會(huì)緩存數(shù)據(jù)吠式,當(dāng)有第二次請(qǐng)求的時(shí)候,客戶端會(huì)額外在請(qǐng)求頭加上If-Modified-Since
或者If-None-Match
抽米,If-Modified-Since
會(huì)攜帶緩存的最后修改時(shí)間特占,服務(wù)端會(huì)把這個(gè)時(shí)間和實(shí)際文件的最后修改時(shí)間進(jìn)行比較。
- 相同就返回狀態(tài)碼304云茸,且不返回?cái)?shù)據(jù)是目,客戶端拿出緩存數(shù)據(jù),渲染頁(yè)面
- 不同就返回狀態(tài)碼200标捺,并且返回?cái)?shù)據(jù)懊纳,客戶端渲染頁(yè)面,并且更新緩存
????當(dāng)然類似的還有Cache-Control
亡容、Expires
和Etag
嗤疯,都是為了校驗(yàn)本地緩存文件和服務(wù)端是否一致,這里就帶過(guò)了闺兢。
NSURLCache
????NSURLCache
是iOS系統(tǒng)提供的內(nèi)存以及磁盤(pán)的綜合緩存機(jī)制茂缚。NSURLCache
對(duì)象被存儲(chǔ)沙盒中Library/cache
目錄下。在我們只需要在didFinishLaunchingWithOptions
函數(shù)里面加上下面的代碼,就可以滿足一般的緩存要求脚囊。(是的龟糕,搞定NSURLCache就是這么簡(jiǎn)單)
NSURLCache * sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024
diskCapacity:100 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
????下面是幾個(gè)常用的API
//設(shè)置內(nèi)存緩存的最大容量
[cache setMemoryCapacity:1024 * 1024 * 20];
//設(shè)置磁盤(pán)緩存的最大容量
[cache setDiskCapacity:1024 * 1024 * 100];
//獲取某個(gè)請(qǐng)求的緩存
[cache cachedResponseForRequest:request];
//清除某個(gè)請(qǐng)求的緩存
[cache removeCachedResponseForRequest:request];
//請(qǐng)求策略,設(shè)置了系統(tǒng)會(huì)自動(dòng)用NSURLCache進(jìn)行數(shù)據(jù)緩存
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
iOS常用的緩存策略
????NSURLRequestCachePolicy
是個(gè)枚舉悔耘,指的是不同的緩存策略讲岁,一共有7種,但是能用的只有4種淮逊。
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
//如果有協(xié)議催首,對(duì)于特定的URL請(qǐng)求扶踊,使用協(xié)議實(shí)現(xiàn)定義的緩存邏輯泄鹏。(默認(rèn)的緩存策略)
NSURLRequestUseProtocolCachePolicy = 0,
//請(qǐng)求僅從原始資源加載URL,不使用任何緩存
NSURLRequestReloadIgnoringLocalCacheData = 1,
//不僅忽略本地緩存秧耗,還要忽略協(xié)議緩存和其他緩存 (未實(shí)現(xiàn))
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4,
//被NSURLRequestReloadIgnoringLocalCacheData替代
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
//無(wú)視緩存的有效期,有緩存就取緩存备籽,沒(méi)有緩存就會(huì)從原始地址加載
NSURLRequestReturnCacheDataElseLoad = 2,
//無(wú)視緩存的有效期,有緩存就取緩存分井,沒(méi)有緩存就視為失敗 (可以用于離線模式)
NSURLRequestReturnCacheDataDontLoad = 3,
//會(huì)從初始地址校驗(yàn)緩存的合法性车猬,合法就用緩存數(shù)據(jù),不合法從原始地址加載數(shù)據(jù) (未實(shí)現(xiàn))
NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};
AFNetworking的緩存策略
????之前寫(xiě)了SDWebImage的源碼解析 里面介紹過(guò)SDWebImage
的緩存策略尺锚,有兩條線根據(jù)時(shí)間和空間來(lái)管理緩存和AFNetworking
很相似珠闰。AFNetworking中AFImageDownloader
使用AFAutoPurgingImageCache
和NSURLCache
管理圖片緩存。
AFNetworking中的NSURLCache
????AFImageDownloader
中設(shè)置NSURLCache
瘫辩,低版本iOS
版本中設(shè)置內(nèi)存容量和磁盤(pán)容量會(huì)閃退(這個(gè)我沒(méi)有考證伏嗜,iOS 7
的手機(jī)還真沒(méi)有)
if ([[[UIDevice currentDevice] systemVersion] compare:@"8.2" options:NSNumericSearch] == NSOrderedAscending) {
return [NSURLCache sharedURLCache];
}
return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024
diskCapacity:150 * 1024 * 1024
diskPath:@"com.alamofire.imagedownloader"];
AFNetworking中的AFAutoPurgingImageCache
????AFAutoPurgingImageCache
是專門(mén)用來(lái)圖片緩存的》パ幔可以看到內(nèi)部有三個(gè)屬性承绸,一個(gè)是用來(lái)裝載AFImageCache
對(duì)象的字典容器,一個(gè)是可以用內(nèi)存空間大小挣轨、一個(gè)同步隊(duì)列军熏。AFAutoPurgingImageCache
在初始化的時(shí)候,會(huì)注冊(cè)UIApplicationDidReceiveMemoryWarningNotification
通知卷扮,收到內(nèi)存警告的時(shí)候會(huì)清除所有緩存荡澎。
@interface AFAutoPurgingImageCache ()
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
@property (nonatomic, assign) UInt64 currentMemoryUsage;
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end
????AFCachedImage
是單個(gè)圖片緩存對(duì)象
@property (nonatomic, strong) UIImage *image;
//標(biāo)志符(這個(gè)值就是圖片的請(qǐng)路徑 request.URL.absoluteString)
@property (nonatomic, strong) NSString *identifier;
//圖片大小
@property (nonatomic, assign) UInt64 totalBytes;
//緩存日期
@property (nonatomic, strong) NSDate *lastAccessDate;
//當(dāng)前可用內(nèi)存空間大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;
????來(lái)看看AFCachedImage
初始化的時(shí)候。iOS
使用圖標(biāo)標(biāo)準(zhǔn)是ARGB_8888
晤锹,即一像素占位4個(gè)字節(jié)摩幔。內(nèi)存大小 = 寬 * 高 * 每像素字節(jié)數(shù)。
-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
if (self = [self init]) {
self.image = image;
self.identifier = identifier;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;
CGFloat bytesPerSize = imageSize.width * imageSize.height;
self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
self.lastAccessDate = [NSDate date];
}
return self;
}
????來(lái)看看添加緩存的代碼抖甘,用了dispatch_barrier_async
柵欄函數(shù)將添加操作和刪除緩存操作分割開(kāi)來(lái)热鞍。每添加一個(gè)緩存對(duì)象,都重新計(jì)算當(dāng)前緩存大小和可用空間大小。當(dāng)內(nèi)存超過(guò)設(shè)定值時(shí)薇宠,會(huì)按照日期的倒序來(lái)遍歷緩存圖片偷办,刪除最早日期的緩存,一直到滿足緩存空間為止澄港。
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});
dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];
UInt64 bytesPurged = 0;
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}
YTKNetwork的緩存策略
????YTKNetwork是猿題庫(kù)技術(shù)團(tuán)隊(duì)開(kāi)源的一個(gè)網(wǎng)絡(luò)請(qǐng)求框架椒涯,內(nèi)部封裝了AFNetworking
。它把每個(gè)請(qǐng)求實(shí)例化回梧,管理它的生命周期废岂,也可以管理多個(gè)請(qǐng)求。筆者在一個(gè)電商的PaaS
項(xiàng)目中就是使用YTKNetwork
狱意,它的特點(diǎn)還有支持請(qǐng)求結(jié)果緩存湖苞,支持批量請(qǐng)求,支持多請(qǐng)求依賴等详囤。
準(zhǔn)備請(qǐng)求之前
????先來(lái)看看請(qǐng)求基類YTKRequest
在請(qǐng)求之前做了什么
- (void)start {
//忽略緩存的標(biāo)志 手動(dòng)設(shè)置 是否利用緩存
if (self.ignoreCache) {
[self startWithoutCache];
return;
}
// 還有未完成的請(qǐng)求 是否還有未完成的請(qǐng)求
if (self.resumableDownloadPath) {
[self startWithoutCache];
return;
}
//加載緩存是否成功
if (![self loadCacheWithError:nil]) {
[self startWithoutCache];
return;
}
_dataFromCache = YES;
dispatch_async(dispatch_get_main_queue(), ^{
//將請(qǐng)求數(shù)據(jù)寫(xiě)入文件
[self requestCompletePreprocessor];
[self requestCompleteFilter];
//這個(gè)時(shí)候直接去相應(yīng) 請(qǐng)求成功的delegate和block 财骨,沒(méi)有發(fā)送請(qǐng)求
YTKRequest *strongSelf = self;
[strongSelf.delegate requestFinished:strongSelf];
if (strongSelf.successCompletionBlock) {
strongSelf.successCompletionBlock(strongSelf);
}
//將block置空
[strongSelf clearCompletionBlock];
});
}
緩存數(shù)據(jù)寫(xiě)入文件
- (void)requestCompletePreprocessor {
[super requestCompletePreprocessor];
if (self.writeCacheAsynchronously) {
dispatch_async(ytkrequest_cache_writing_queue(), ^{
[self saveResponseDataToCacheFile:[super responseData]];
});
} else {
[self saveResponseDataToCacheFile:[super responseData]];
}
}
????ytkrequest_cache_writing_queue
是一個(gè)優(yōu)先級(jí)比較低的串行隊(duì)列,當(dāng)標(biāo)志dataFromCache
為YES
的時(shí)候藏姐,確定能拿到數(shù)據(jù)隆箩,在這個(gè)串行隊(duì)列中異步的寫(xiě)入文件。來(lái)看看寫(xiě)入緩存的具體操作羔杨。
- (void)saveResponseDataToCacheFile:(NSData *)data {
if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
if (data != nil) {
@try {
// New data will always overwrite old data.
[data writeToFile:[self cacheFilePath] atomically:YES];
YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
metadata.version = [self cacheVersion];
metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
metadata.creationDate = [NSDate date];
metadata.appVersionString = [YTKNetworkUtils appVersionString];
[NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];
} @catch (NSException *exception) {
YTKLog(@"Save cache failed, reason = %@", exception.reason);
}
}
}
}
????除了請(qǐng)求數(shù)據(jù)文件捌臊,YTK
還會(huì)生成一個(gè)記錄緩存數(shù)據(jù)信息的元數(shù)據(jù)YTKCacheMetadata
對(duì)象。YTKCacheMetadata
記錄了緩存的版本號(hào)兜材、敏感信息理澎、緩存日期和App的版本號(hào)。
@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;
????然后把請(qǐng)求方法护姆、請(qǐng)求域名矾端、請(qǐng)求URL和請(qǐng)求參數(shù)組成的字符串進(jìn)行一次MD5
加密,作為緩存文件的名稱卵皂。YTKCacheMetadata
和緩存文件同名秩铆,多了一個(gè).metadata
的后綴作為區(qū)分。文件寫(xiě)入的路徑是沙盒中Library/LazyRequestCache
目錄下灯变。
- (NSString *)cacheFileName {
NSString *requestUrl = [self requestUrl];
NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl;
id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]];
NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",
(long)[self requestMethod], baseUrl, requestUrl, argument];
NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo];
return cacheFileName;
}
校驗(yàn)緩存
????回到start方法中殴玛,loadCacheWithError
是校驗(yàn)緩存能不能成功加載出來(lái),loadCacheWithError
中會(huì)調(diào)用validateCacheWithError
來(lái)檢驗(yàn)緩存的合法性添祸,校驗(yàn)的依據(jù)正是YTKCacheMetadata
和cacheTimeInSeconds
滚粟。要想使用緩存數(shù)據(jù),請(qǐng)求實(shí)例要重寫(xiě)cacheTimeInSeconds
設(shè)置一個(gè)大于0的值刃泌,而且緩存還支持版本凡壤、App的版本署尤。在實(shí)際項(xiàng)目上應(yīng)用,get
請(qǐng)求實(shí)例設(shè)置一個(gè)cacheTimeInSeconds
就夠用了亚侠。
- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
// Date
NSDate *creationDate = self.cacheMetadata.creationDate;
NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
if (duration < 0 || duration > [self cacheTimeInSeconds]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}];
}
return NO;
}
// Version
long long cacheVersionFileContent = self.cacheMetadata.version;
if (cacheVersionFileContent != [self cacheVersion]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}];
}
return NO;
}
// Sensitive data
NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
if (sensitiveDataString || currentSensitiveDataString) {
// If one of the strings is nil, short-circuit evaluation will trigger
if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
}
return NO;
}
}
// App version
NSString *appVersionString = self.cacheMetadata.appVersionString;
NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
if (appVersionString || currentAppVersionString) {
if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}];
}
return NO;
}
}
return YES;
}
清除緩存
????因?yàn)榫彺娴哪夸浭?code>Library/LazyRequestCache曹体,清除緩存就直接清空目錄下所有文件就可以了。調(diào)用[[YTKNetworkConfig sharedConfig] clearCacheDirPathFilter]
就行硝烂。
結(jié)語(yǔ)
????緩存的本質(zhì)是用空間換取時(shí)間箕别。大學(xué)里面學(xué)過(guò)的《計(jì)算機(jī)組成原理》中就有介紹cache
,除了磁盤(pán)和內(nèi)存,還有L1和L2滞谢,對(duì)于iOS開(kāi)發(fā)者來(lái)說(shuō)串稀,一般關(guān)注disk
和memory
就夠了。閱讀SDWebImage狮杨、AFNetworking母截、YTKNetwork
的源碼后,可以看出他們都非常重視數(shù)據(jù)的多線程的讀寫(xiě)安全禾酱,在做深度優(yōu)化時(shí)候微酬,因地制宜绘趋,及時(shí)清理緩存文件颤陶。