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. 無數開發者嘗試本身作一個醜陋而脆弱的系統來實現網絡緩存的功能,卻不知NSURLCache只要兩行代碼就能搞定,而且好上100倍。甚至更多的開發者根本不知道網絡緩存的好處,歷來沒有嘗試過解決方案,致使他們的App向服務器發出無數沒必要要的請求。ios
上面是引用Mattt
大神在NSHipster介紹NSURLCache時的原話。git
先看看服務端的緩存策略。當第一次請求後,客戶端會緩存數據,當有第二次請求的時候,客戶端會額外在請求頭加上If-Modified-Since
或者If-None-Match
,If-Modified-Since
會攜帶緩存的最後修改時間,服務端會把這個時間和實際文件的最後修改時間進行比較。github
固然相似的還有Cache-Control
、Expires
和Etag
,都是爲了校驗本地緩存文件和服務端是否一致,這裏就帶過了。數組
NSURLCache
是iOS系統提供的內存以及磁盤的綜合緩存機制。NSURLCache
對象被存儲沙盒中Library/cache
目錄下。在咱們只須要在didFinishLaunchingWithOptions
函數裏面加上下面的代碼,就能夠知足通常的緩存要求。(是的,搞定NSURLCache就是這麼簡單)緩存
NSURLCache * sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 *1024 diskCapacity:100 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
複製代碼
下面是幾個經常使用的API安全
//設置內存緩存的最大容量
[cache setMemoryCapacity:1024 * 1024 * 20];
//設置磁盤緩存的最大容量
[cache setDiskCapacity:1024 * 1024 * 100];
//獲取某個請求的緩存
[cache cachedResponseForRequest:request];
//清除某個請求的緩存
[cache removeCachedResponseForRequest:request];
//請求策略,設置了系統會自動用NSURLCache進行數據緩存
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
複製代碼
NSURLRequestCachePolicy
是個枚舉,指的是不一樣的緩存策略,一共有7種,可是能用的只有4種。bash
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
//若是有協議,對於特定的URL請求,使用協議實現定義的緩存邏輯。(默認的緩存策略)
NSURLRequestUseProtocolCachePolicy = 0,
//請求僅從原始資源加載URL,不使用任何緩存
NSURLRequestReloadIgnoringLocalCacheData = 1,
//不只忽略本地緩存,還要忽略協議緩存和其餘緩存 (未實現)
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4,
//被NSURLRequestReloadIgnoringLocalCacheData替代
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
//無視緩存的有效期,有緩存就取緩存,沒有緩存就會從原始地址加載
NSURLRequestReturnCacheDataElseLoad = 2,
//無視緩存的有效期,有緩存就取緩存,沒有緩存就視爲失敗 (能夠用於離線模式)
NSURLRequestReturnCacheDataDontLoad = 3,
//會從初始地址校驗緩存的合法性,合法就用緩存數據,不合法從原始地址加載數據 (未實現)
NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};
複製代碼
以前寫了SDWebImage的源碼解析 裏面介紹過SDWebImage
的緩存策略,有兩條線根據時間和空間來管理緩存和AFNetworking
很類似。AFNetworking中AFImageDownloader
使用AFAutoPurgingImageCache
和NSURLCache
管理圖片緩存。服務器
AFImageDownloader
中設置NSURLCache
,低版本iOS
版本中設置內存容量和磁盤容量會閃退(這個我沒有考證,iOS 7
的手機還真沒有)網絡
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"];
複製代碼
AFAutoPurgingImageCache
是專門用來圖片緩存的。能夠看到內部有三個屬性,一個是用來裝載AFImageCache
對象的字典容器,一個是能夠用內存空間大小、一個同步隊列。AFAutoPurgingImageCache
在初始化的時候,會註冊UIApplicationDidReceiveMemoryWarningNotification
通知,收到內存警告的時候會清除全部緩存。多線程
@interface AFAutoPurgingImageCache ()
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
@property (nonatomic, assign) UInt64 currentMemoryUsage;
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end
複製代碼
AFCachedImage
是單個圖片緩存對象
@property (nonatomic, strong) UIImage *image;
//標誌符(這個值就是圖片的請路徑 request.URL.absoluteString)
@property (nonatomic, strong) NSString *identifier;
//圖片大小
@property (nonatomic, assign) UInt64 totalBytes;
//緩存日期
@property (nonatomic, strong) NSDate *lastAccessDate;
//當前可用內存空間大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;
複製代碼
來看看AFCachedImage
初始化的時候。iOS
使用圖標標準是ARGB_8888
,即一像素佔位4個字節。內存大小 = 寬×高×每像素字節數。
-(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;
}
複製代碼
來看看添加緩存的代碼,用了dispatch_barrier_async
柵欄函數將添加操做和刪除緩存操做分割開來。每添加一個緩存對象,都從新計算當前緩存大小和可用空間大小。當內存超過設定值時,會按照日期的倒序來遍歷緩存圖片,刪除最先日期的緩存,一直到知足緩存空間爲止。
- (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是猿題庫技術團隊開源的一個網絡請求框架,內部封裝了AFNetworking
。它把每一個請求實例化,管理它的生命週期,也能夠管理多個請求。筆者在一個電商的PaaS
項目中就是使用YTKNetwork
,它的特色還有支持請求結果緩存,支持批量請求,支持多請求依賴等。
先來看看請求基類YTKRequest
在請求以前作了什麼
- (void)start
{
//忽略緩存的標誌 手動設置 是否利用緩存
if (self.ignoreCache)
{
[self startWithoutCache];
return;
}
// 還有未完成的請求 是否還有未完成的請求
if (self.resumableDownloadPath)
{
[self startWithoutCache];
return;
}
//加載緩存是否成功
if (![self loadCacheWithError:nil])
{
[self startWithoutCache];
return;
}
_dataFromCache = YES;
dispatch_async(dispatch_get_main_queue(), ^{
//將請求數據寫入文件
[self requestCompletePreprocessor];
[self requestCompleteFilter];
//這個時候直接去相應 請求成功的delegate和block ,沒有發送請求
YTKRequest *strongSelf = self;
[strongSelf.delegate requestFinished:strongSelf];
if (strongSelf.successCompletionBlock)
{
strongSelf.successCompletionBlock(strongSelf);
}
//將block置空
[strongSelf clearCompletionBlock];
});
}
複製代碼
- (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
是一個優先級比較低的串行隊列,當標誌dataFromCache
爲YES
的時候,肯定能拿到數據,在這個串行隊列中異步的寫入文件。來看看寫入緩存的具體操做。
- (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);
}
}
}
}
複製代碼
除了請求數據文件,YTK
還會生成一個記錄緩存數據信息的元數據YTKCacheMetadata
對象。YTKCacheMetadata
記錄了緩存的版本號、敏感信息、緩存日期和App的版本號。
@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;
複製代碼
而後把請求方法、請求域名、請求URL和請求參數組成的字符串進行一次MD5
加密,做爲緩存文件的名稱。YTKCacheMetadata
和緩存文件同名,多了一個.metadata
的後綴做爲區分。文件寫入的路徑是沙盒中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;
}
複製代碼
回到start方法中,loadCacheWithError
是校驗緩存能不能成功加載出來,loadCacheWithError
中會調用validateCacheWithError
來檢驗緩存的合法性,校驗的依據正是YTKCacheMetadata
和cacheTimeInSeconds
。要想使用緩存數據,請求實例要重寫cacheTimeInSeconds
設置一個大於0的值,並且緩存還支持版本、App的版本。在實際項目上應用,get
請求實例設置一個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;
}
複製代碼
由於緩存的目錄是Library/LazyRequestCache
,清除緩存就直接清空目錄下全部文件就能夠了。調用[[YTKNetworkConfig sharedConfig] clearCacheDirPathFilter]
就行。
緩存的本質是用空間換取時間。大學裏面學過的《計算機組成原理》中就有介紹cache
,除了磁盤和內存,還有L1和L2,對於iOS開發者來講,通常關注disk
和memory
就夠了。閱讀SDWebImage、AFNetworking、YTKNetwork
的源碼後,能夠看出他們都很是重視數據的多線程的讀寫安全,在作深度優化時候,因地制宜,及時清理緩存文件。