AFNetWorking是如何进行数据缓存的--之AFImageCache & NSURLCache 详解
dajianshi
8年前
<p>如果你是一个正在使用由Matt Thompson开发的网络库 AFNetWorking(如果你还没有使用,那你还在等什么?)的iOS开发者,也许你一直很好奇和困惑它的缓存机制,并且想要了解如何更好地充分利用它?</p> <h3><strong>AFNetworking实际上利用了两套单独的缓存机制:</strong></h3> <ul> <li> <p>AFImagecache : 继承于NSCache,AFNetworking的图片内存缓存的类。</p> </li> <li> <p>NSURLCache : NSURLConnection的默认缓存机制,用于存储NSURLResponse对象:一个默认缓存在内存,并且可以通过一些配置操作可以持久缓存到磁盘的类。</p> </li> </ul> <h3><strong>AFImageCache是如何工作的?</strong></h3> <p>AFImageCache属于UIImageView+AFNetworking的一部分,继承于NSCache,以URL(从NSURLRequest对象中获取)字符串作为key值来存储UIImage对象。 AFImageCache的定义如下:(这里我们声明了一个2M内存、100M磁盘空间的NSURLCache对象。)</p> <pre> <code class="language-objectivec">@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)]; } }</code></pre> <p>AFImageCache是NSCache的私有实现,它把所有可访问的UIImage对象存入NSCache中,并控制着UIImage对象应该在何时释放,如果UIImage对象释放的时候你希望去做一些监听操作,你可以实现NSCacheDelegate的 cache:willEvictObject 代理方法。Matt Thompson已经谦虚的告诉我在AFNetworking2.1版本中可通过setSharedImageCache方法来配置AFImageCache,这里是 AFN2.2.1中的UIImageView+AFNetworking文档。</p> <h3><strong>NSURLCache</strong></h3> <p>AFNetworking使用了NSURLConnection,它利用了iOS原生的缓存机制,并且NSURLCache缓存了服务器返回的NSURLRespone对象。NSURLCache 的shareCache方法是默认开启的,你可以利用它来获取每一个NSURLConnection对象的URL内容。让人不爽的是,它的默认配置是缓存到内存而且并没有写入到磁盘。为了tame the beast(驯服野兽?不太懂),增加可持续性,你可以在AppDelegate中简单地声明一个共享的NSURLCache对象,像这样:</p> <pre> <code class="language-objectivec">NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024 diskCapacity:100 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache];</code></pre> <h3><strong>设置NSURLRequest对象的缓存策略</strong></h3> <p>NSURLCache 将对每一个NSURLRequest对象遵守缓存策略(NSURLRequestCachePolicy),策略如下所示:</p> <pre> <code class="language-objectivec">- NSURLRequestUseProtocolCachePolicy 默认的缓存策略,对特定的URL请求使用网络协议中实现的缓存逻辑 - NSURLRequestReloadIgnoringLocalCacheData 忽略本地缓存,重新请请求 - NSURLRequestReloadIgnoringLocalAndRemoteCacheData 忽略本地和远程缓存,重新请求 - NSURLRequestReturnCacheDataElseLoad 有缓存则从中加载,如果没有则去请求 - NSURLRequestReturnCacheDataDontLoad 无网络状态下不去请求,一直加载本地缓存数据无论其是否存在 - NSURLRequestReloadRevalidatingCacheData 默从原始地址确认缓存数据的合法性之后,缓存数据才可使用,否则请求原始地址</code></pre> <h3><strong>用NSURLCache缓存数据到磁盘</strong></h3> <p>Cache-Control HTTP Header</p> <p>Cache-Controlheader或Expires header存在于服务器返回的HTTP response header中,来用于客户端的缓存工作(前者优先级要高于后者),这里面有很多地方需要注意,Cache-Control可以拥有被定义为类似max-age的参数(在更新响应之前要缓存多长时间), public/private 访问或者是non-cache(不缓存响应数据), <a href="/misc/goto?guid=4959723268802297174" rel="nofollow,noindex">这里</a> 对HTTP cache headers进行了很好的介绍。</p> <p>继承并控制NSURLCache</p> <p>如果你想跳过Cache-Control,并且想要自己来制定规则读写一个带有NSURLResponse对象的NSURLCache,你可以继承NSURLCache。下面有个例子,使用 CACHE_EXPIRES来判断在获取源数据之前对缓存数据保留多长时间.(感谢 <a href="/misc/goto?guid=4959723268892392525" rel="nofollow,noindex">Mattt Thompson</a> 的回复)</p> <pre> <code class="language-objectivec"> @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</code></pre> <p>现在你有了属于自己的NSURLCache的子类,不要忘了在AppDelegate中初始化并且使用它。</p> <h3><strong>在缓存之前重写NSURLResponse</strong></h3> <p>-connection:willCacheResponse 代理方法是在被缓存之前用于截断和编辑由NSURLConnection创建的NSURLCacheResponse的地方。 对NSURLCacheResponse进行处理并返回一个可变的拷贝对象(代码来自 <a href="/misc/goto?guid=4959545512867337845" rel="nofollow,noindex">NSHipster blog</a> )</p> <pre> <code class="language-objectivec">- (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; }</code></pre> <h3><strong>禁用NSURLCache</strong></h3> <p>不想使用NSURLCache?不为所动?好吧,你可以禁用NSURLCache,只需要将内存和磁盘空间设置为0就行了.</p> <pre> <code class="language-objectivec">NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]; [NSURLCache setSharedURLCache:sharedCache];</code></pre> <h3><strong>总结</strong></h3> <p>我写这篇博客的目的是为iOS社区贡献绵薄之力,并总结了我是如何来处理关于AFNetworking缓存问题的。我们有个内部App在加载了大量图片后,出现了内存和性能问题,而我的主要职责是诊断这个App的缓存行为,在研究过程中,我在网上搜索了很多资料并且做了很多调试,在我汇总之后就写到了这篇博客中,我希望这篇文章可以为开发者使用AFNetworking时提供一些帮助,真心希望对你们有用!</p> <p> </p> <p>来自:http://www.cocoachina.com/ios/20161101/17906.html</p> <p> </p>