如果你是一名使用Mattt
Thompson網絡框架AFNetworking
的iOS開發者(如果你不是,那還等什麼呢?),也許你對這個框架中的緩存機制很好奇或者疑惑,並想學習如何在自己的app中充分利用這種機制。
AFNetworking
實際上使用了兩個獨立的緩存機制:
- AFImagecache:一個提供圖片內存緩存的類,繼承自
NSCache
。 - NSURLCache:
NSURLConnection's
默認的URL緩存機制,用於存儲NSURLResponse
對象:一個默認緩存在內存,通過配置可以緩存到磁盤的類。
爲了理解每個緩存系統是如何工作的,我們看一下他們是如何定義的。
AFImageCache是如何工作的
AFImageCache
是UIImageView+AFNetworking
分類的一部分。它繼承自NSCache
,通過一個URL字符串作爲它的key(從NSURLRequest
中獲取)來存儲UIImage
對象。
AFImageCache定義:
@interface AFImageCache : NSCache <AFImageCache>
// singleton instantiation :
+ (id <AFImageCache>)sharedImageCache {
static AFImageCache *_af_defaultImageCache = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_af_defaultImageCache = [[AFImageCache alloc] init];
// clears out cache on memory warning :
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * __unused notification) {
[_af_defaultImageCache removeAllObjects];
}];
});
// key from [[NSURLRequest URL] absoluteString] :
static inline NSString * AFImageCacheKeyFromURLRequest(NSURLRequest *request) {
return [[request URL] absoluteString];
}
@implementation AFImageCache
// write to cache if proper policy on NSURLRequest :
- (UIImage *)cachedImageForRequest:(NSURLRequest *)request {
switch ([request cachePolicy]) {
case NSURLRequestReloadIgnoringCacheData:
case NSURLRequestReloadIgnoringLocalAndRemoteCacheData:
return nil;
default:
break;
}
return [self objectForKey:AFImageCacheKeyFromURLRequest(request)];
}
// read from cache :
- (void)cacheImage:(UIImage *)image
forRequest:(NSURLRequest *)request {
if (image && request) {
[self setObject:image forKey:AFImageCacheKeyFromURLRequest(request)];
}
}
AFImageCache
從 AFNetworking
2.1開始可以進行配置了。有一個公共方法setSharedImageCache
。詳細文檔可以看這裏 。它把所有可訪問的UIImage
對象存到了NSCache
。當UIImage
對象釋放之後NSCache
會進行處理。如果你想觀察images什麼時候釋放,可以實現NSCacheDelegate
的cache:willEvictObject
方法
NSURLCache如何工作
默認是可以的,但最好還是手動配置一下
既然AFNetworking
使用NSURLConnection
,它利用了原生的緩存機制NSURLCache
。NSURLCache
緩存了從服務器返回的NSURLResponse
對象。
NSURLCache
的shareCache
方法默認是可以使用的,緩存獲取的內容。不幸的是,它的默認配置只是緩存在內存並沒有寫到硬盤。爲了解決這個問題,你可以聲明一個 sharedCache
,像這樣:
1 2 3 4 |
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024 diskCapacity:100 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache]; |
這樣,我們聲明瞭一個2M內存,100M磁盤空間的NSURLCache
。
對NSURLRequest對象設置緩存策略
NSURLCache
對每個NSURLRequest
對象都會遵守緩存策略(NSURLRequestCachePolicy)。策略定義如下:
- NSURLRequestUseProtocolCachePolicy:指定定義在協議實現裏的緩存邏輯被用於URL請求。這是URL請求的默認策略
- NSURLRequestReloadIgnoringLocalCacheData:忽略本地緩存,從源加載
- NSURLRequestReloadIgnoringLocalAndRemoteCacheData:忽略本地&服務器緩存,從源加載
- NSURLRequestReturnCacheDataElseLoad:先從緩存加載,如果沒有緩存,從源加載
- NSURLRequestReturnCacheDataDontLoad離線模式,加載緩存數據(無論是否過期),不從源加載
- NSURLRequestReloadRevalidatingCacheData存在的緩存數據先確認有效性,無效的話從源加載
用NSURLCache緩存到磁盤
Cache-Control HTTP Header
Cache-Control
或者Expires
header
必須在從服務器返回的 HTTP response header 中,用於客戶端的緩存(Cache-Control header 優先權高於 Expires header)。這裏邊有很多需要注意的地方,Cache Control
可以有被定義爲 max-age
的參數(在更新響應之前緩存多長時間),public/private
訪問,或者 no-cache
(不緩存響應數據),這裏有一個關於HTTP
cache headers的文章。
Subclass NSURLCache for Ultimate Control
如果你想繞過 Cache-Control
需求,定義你自己的規則來讀寫一個帶有 NSURLResponse
對象的NSURLCache
,你可以繼承 NSURLCache
。
這裏有個例子,使用 CACHE_EXPIRES
來判斷在獲取源數據之前對緩存數據保留多長時間。
(感謝 Mattt Thompson的反饋)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
@interface CustomURLCache : NSURLCache static NSString * const CustomURLCacheExpirationKey = @"CustomURLCacheExpiration"; static NSTimeInterval const CustomURLCacheExpirationInterval = 600; @implementation CustomURLCache + (instancetype)standardURLCache { static CustomURLCache *_standardURLCache = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _standardURLCache = [[CustomURLCache alloc] initWithMemoryCapacity:(2 * 1024 * 1024) diskCapacity:(100 * 1024 * 1024) diskPath:nil]; } return _standardURLCache; } #pragma mark - NSURLCache - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request { NSCachedURLResponse *cachedResponse = [super cachedResponseForRequest:request]; if (cachedResponse) { NSDate* cacheDate = cachedResponse.userInfo[CustomURLCacheExpirationKey]; NSDate* cacheExpirationDate = [cacheDate dateByAddingTimeInterval:CustomURLCacheExpirationInterval]; if ([cacheExpirationDate compare:[NSDate date]] == NSOrderedAscending) { [self removeCachedResponseForRequest:request]; return nil; } } } return cachedResponse; } - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:cachedResponse.userInfo]; userInfo[CustomURLCacheExpirationKey] = [NSDate date]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:cachedResponse.response data:cachedResponse.data userInfo:userInfo storagePolicy:cachedResponse.storagePolicy]; [super storeCachedResponse:modifiedCachedResponse forRequest:request]; } @end |
既然你有了自己的 NSURLCache
子類,不要忘了在AppDelegate裏邊初始化並使用它
1 2 3 4 |
CustomURLCache *URLCache = [[CustomURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024 diskCapacity:100 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:URLCache]; |
Overriding the NSURLResponse before caching
-connection:willCacheResponse
代理方法是在被緩存之前用於截斷和編輯由NSURLConnection
創建的NSURLCacheResponse
的地方。爲了編輯NSURLCacheResponse
,返回一個可變的拷貝,如下(代碼來自NSHipster
blog):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { NSMutableDictionary *mutableUserInfo = [[cachedResponse userInfo] mutableCopy]; NSMutableData *mutableData = [[cachedResponse data] mutableCopy]; NSURLCacheStoragePolicy storagePolicy = NSURLCacheStorageAllowedInMemoryOnly; // ... return [[NSCachedURLResponse alloc] initWithResponse:[cachedResponse response] data:mutableData userInfo:mutableUserInfo storagePolicy:storagePolicy]; } // If you do not wish to cache the NSURLCachedResponse, just return nil from the delegate function: - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { return nil; } |
Disabling NSURLCache
不想使用 NSURLCache
,可以,只需要將內存和磁盤空間容量設爲零就可以了
1 2 3 4 |
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache]; |
總結
我寫這篇博客是爲了iOS社區貢獻一份力,總結了一下我在處理關於 AFNetworking
緩存相關的問題。我們有個內部App加載了好多圖片,導致內存問題以及性能問題。我主要職責就是診斷這個App的緩存行爲。在這個研究過程中,我在網上搜索了好多資料並且做了好多調試。然後我總結之後寫到了這篇博客中。我希望這篇文章能夠爲其他人用AFNetworking
的時候提供幫助,真心希望對你們有用處!