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>