原文-Tim Brandt《How Does Caching Work in AFNetworking? : AFImageCache & NSUrlCache Explained》
如果你是一個正在使用由Matt Thompson開發的網絡庫 AFNetWorking(如果你還沒有使用,那你還在等什麼?)的iOS開發者,也許你一直很好奇和困惑它的緩存機制,並且想要了解如何更好地充分利用它?
AFNetworking實際上利用了兩套單獨的緩存機制:
AFImagecache : 繼承於NSCache,AFNetworking的圖片內存緩存的類。
NSURLCache : NSURLConnection的默認緩存機制,用於存儲NSURLResponse對象:一個默認緩存在內存,並且可以通過一些配置操作可以持久緩存到磁盤的類。
AFImageCache是如何工作的?
AFImageCache屬於UIImageView+AFNetworking的一部分,繼承於NSCache,以URL(從NSURLRequest對象中獲取)字符串作為key值來存儲UIImage對象。 AFImageCache的定義如下:(這裡我們聲明了一個2M內存、100M磁盤空間的NSURLCache對象。)
@interface AFImageCache : NSCache // singleton instantiation : + (id )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是NSCache的私有實現,它把所有可訪問的UIImage對象存入NSCache中,並控制著UIImage對象應該在何時釋放,如果UIImage對象釋放的時候你希望去做一些監聽操作,你可以實現NSCacheDelegate的 cache:willEvictObject 代理方法。Matt Thompson已經謙虛的告訴我在AFNetworking2.1版本中可通過setSharedImageCache方法來配置AFImageCache,這裡是 AFN2.2.1中的UIImageView+AFNetworking文檔。
NSURLCache
AFNetworking使用了NSURLConnection,它利用了iOS原生的緩存機制,並且NSURLCache緩存了服務器返回的NSURLRespone對象。NSURLCache 的shareCache方法是默認開啟的,你可以利用它來獲取每一個NSURLConnection對象的URL內容。讓人不爽的是,它的默認配置是緩存到內存而且並沒有寫入到磁盤。為了tame the beast(馴服野獸?不太懂),增加可持續性,你可以在AppDelegate中簡單地聲明一個共享的NSURLCache對象,像這樣:
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024 diskCapacity:100 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache];
設置NSURLRequest對象的緩存策略
NSURLCache 將對每一個NSURLRequest對象遵守緩存策略(NSURLRequestCachePolicy),策略如下所示:
- NSURLRequestUseProtocolCachePolicy 默認的緩存策略,對特定的URL請求使用網絡協議中實現的緩存邏輯 - NSURLRequestReloadIgnoringLocalCacheData 忽略本地緩存,重新請請求 - NSURLRequestReloadIgnoringLocalAndRemoteCacheData 忽略本地和遠程緩存,重新請求 - NSURLRequestReturnCacheDataElseLoad 有緩存則從中加載,如果沒有則去請求 - NSURLRequestReturnCacheDataDontLoad 無網絡狀態下不去請求,一直加載本地緩存數據無論其是否存在 - NSURLRequestReloadRevalidatingCacheData 默從原始地址確認緩存數據的合法性之後,緩存數據才可使用,否則請求原始地址
用NSURLCache緩存數據到磁盤
Cache-Control HTTP Header
Cache-Controlheader或Expires header存在於服務器返回的HTTP response header中,來用於客戶端的緩存工作(前者優先級要高於後者),這裡面有很多地方需要注意,Cache-Control可以擁有被定義為類似max-age的參數(在更新響應之前要緩存多長時間), public/private 訪問或者是non-cache(不緩存響應數據),這裡對HTTP cache headers進行了很好的介紹。
繼承並控制NSURLCache
如果你想跳過Cache-Control,並且想要自己來制定規則讀寫一個帶有NSURLResponse對象的NSURLCache,你可以繼承NSURLCache。下面有個例子,使用 CACHE_EXPIRES來判斷在獲取源數據之前對緩存數據保留多長時間.(感謝 Mattt Thompson的回復)
@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中初始化並且使用它。
在緩存之前重寫NSURLResponse
-connection:willCacheResponse 代理方法是在被緩存之前用於截斷和編輯由NSURLConnection創建的NSURLCacheResponse的地方。 對NSURLCacheResponse進行處理並返回一個可變的拷貝對象(代碼來自NSHipster blog)
- (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; }
禁用NSURLCache
不想使用NSURLCache?不為所動?好吧,你可以禁用NSURLCache,只需要將內存和磁盤空間設置為0就行了.
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache];
總結
我寫這篇博客的目的是為iOS社區貢獻綿薄之力,並總結了我是如何來處理關於AFNetworking緩存問題的。我們有個內部App在加載了大量圖片後,出現了內存和性能問題,而我的主要職責是診斷這個App的緩存行為,在研究過程中,我在網上搜索了很多資料並且做了很多調試,在我匯總之後就寫到了這篇博客中,我希望這篇文章可以為開發者使用AFNetworking時提供一些幫助,真心希望對你們有用!