之前的文章中,我们解析了AFNetworking的图片缓存实现.今天我们来看看这些实现中探讨两个有意思的问题.
3.1 避免同一链接资源短时间内重复下载
在AFNetworking的图片缓存实现中,我们发现了一个很有意思的类就是AFImageDownloaderMergedTask.它通过如下操作避免了同一资源的重复请求:
- 在发送请求时,创建了对应于请求链接URLIdentifier的mergedTasks对象,同时将该请求的回调使用AFImageDownloaderResponseHandler进行封装并保存在mergedTasks.responseHandlers中,并保存在全局保存mergedTasks中;
- 当再次针发起请求时,首先检查一下对应于URLIdentifier的mergedTasks对象是否已经存在:如果不存在重复上述步骤;如果存在则将该请求的回调封装成新AFImageDownloaderResponseHandler对象,添加到mergedTasks.responseHandlers中,然后方法直接返回,不再发起新的请求.当请求结束时只需要遍历对应的mergedTasks.responseHandlers逐个执行回调即可.
这样做可以避免同一资源的重复请求,尤其是对于这种情况频繁发生的时候,可以节约应用资源,同时减少响应时间,提高执行效率.
这种需求在我们开发中经常遇到,例如:
- 频繁获取地理位置信息:在应用中,由于埋点等业务需要,我们需要频繁地获取对位置信息,而持续获取地理位置信息不仅没有必要,而且耗电,降低用户对应用的认可度,最重要的是地理位置的获取可能会很耗时,在需要频繁进行埋点操作的功能块里,如果每次需要地理位置信息都需要获取一次的话,耗时将会不断叠加.这时我们就可以仿照上述实现做一个缓存,将获取地理位置的回调都缓存起来,等到获取到地理位置信息时,逐个遍历并执行回调,而且由于现实中地理位置信息不可能在短时间内发生远距离的变化,所以还可以添加一个时间判断,在获取到地理位置信息未来一段时间(例如5min)内,都使用上次获取到的地理位置信息,这样就可以减少不必要的系统开销,提升用户体验;
- 同一事件的异常多次触发.例如由于用户手速过快,界面临时性卡顿或者系统处理问题等原因,同一个请求会连续向服务器发送两次,这样的异常会比较隐蔽,一般的做法是人为地限制同一事件允许第二次触发的时间间隔,可是这种处理并不是很友好,原因是:(1)间间隔间隔究竟设置多少很难有一个准确的人为界定;(2)有时候确实需要连续触发某一事件(比如在某一个点击事件点击前后需要两次埋点).所以此时我们就可以利用(url+params.json).md5来做为回调的identifier存储回调,如果是异常请求,那么两次的identifier是相同的,可以选择抛弃或者存储(根据实际需求),而如果两次的identifier不一样,则证明确实是两个不同的需求,则需要发送两次请求.这样就可以很好地解决上边的两个问题.
3.2 AFN中图片加载中是否有磁盘缓存?
在上一篇文章中我们看到,其实AFN使用了NSURLCache来进行缓存处理.但是在处理中好像只做了内存的缓存处理,并没有做磁盘的缓存处理,显然这和SD的缓存机制不太一样,那么AFN到底有没有磁盘缓存机制呢?
我们在模拟器上使用AFNImageDownloader加载一张网络图片,然后查看一下磁盘缓存路径是否有缓存.
- (void)loadData { NSString *str = @"http://pic31.nipic.com/20130801/11604791_100539834000_2.jpg"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [[AFImageDownloader defaultInstance] downloadImageForURLRequest:request success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) { self.imageView.image = responseObject; NSLog(@"home directory : %@", NSHomeDirectory()); } failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) { }]; }
在Libray--Caches--{bundle Id}--com.alamofire.imagedownloader路径下发现了Cache.db这个数据库的存在.使用数据库工具(这里使用DB Broswer)打开之后,我们就会发现,该数据库中包含了五个数据库的表:
其中表
- sqlite_sequence是sqlite的系统表,在数据库被创建时会被自动创建,主要用来存储数据库中其他表的RowID的最大值(也就是行的最大值),当数据库中行的最大值超过限度时,继续添加数据会报出SQL_FULL错误;
- cfurl_cache_scheme_version:表示协议版本;
- cfurl_cache_response:在该表中保存了entry_ID(主键),hash_value(资源哈希值),storage_policy(缓存策略),request_key(资源链接),time_stamp(时间戳),partition(分区)等信息.在这个表中,可以通过request_key来获取主键entry_ID;
- cfurl_cache_blob_data: 在该表中保存了entry_ID(主键), request_object(请求对象),response_object(响应对象), user_info以及其他信息.我们可以根据entry_ID找到response_object,user_info;
- cfurl_cache_receiver_data:这个表中有三个键entry_ID(主键),isDataOnFS(数据是否缓存在FS上,猜测可能是File System),receiver_data(响应收到的数据存储的位置).在数据库同一级目录下有一个fsCachedData文件夹,而党isDataOnFS为1时,可以在fsCachedData路径下根据receiver_data找到缓存的数据.
所以我们在表cfurl_cache_response根据request_key来获取到entry_ID,然后在表cfurl_cache_blob_data获取到response_object,从表cfurl_cache_receiver_data获取到receiver_data,最后用response_object和receiver_data拼装NSCachedURLResponse.
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:[[request allHTTPHeaderFields] objectForKey:@"Accept"] expectedContentLength:[(NSData *)response_object length] textEncodingName:nil]; NSCachedURLResponse *cachedURLResponse = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:receiver_data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
至此我们就可以获取到完整的NSCachedURLResponse信息.所以,我们通过上面的分析可以知道,其实NSURLCache中对请求进行了缓存操作,而且根据官方文档说明,NSURLCache不仅进行了磁盘缓存,同时也进行了内存的缓存.所有的相关请求数据都会缓存下来,所以我们AFN中是有磁盘缓存的。那么问题来了,既然AFN实现了磁盘缓存,为什么AFN没有从磁盘中获取的操作呢?
可能处于以下考虑:
- AFN的主要功能不是用来处理图片资源的加载,也就没有做的那么细腻;
- 磁盘缓存本身的意义也不是很大.当图片发起加载时,AFN会尝试从内存缓存中进行加载,这是因为如果资源在内存缓存中证明应用的内存空间一直没有被销毁,在一次启动中同一链接中的图片资源发生改变的可能性很低,所以使用内存可以节约同一资源的重复请求,可以节约资源,提升用户体验。而对于磁盘缓存就不一样了,如果需要使用到磁盘缓存则证明应用已经比较久,如果不发起请求无法判断该链接对应的资源是否已经更新,所以不能直接使用磁盘缓存来返回结果;如果发起了请求,再去磁盘缓存也就没有什么意义了.所以AFN直接忽略了磁盘的缓存获取.
如何能让AFN使用磁盘缓存呢?
- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request withReceiptID:(nonnull NSUUID *)receiptID success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure { ... ... ... //3. 如果缓存策略允许从缓存中获取资源,则查看缓存中是否存在资源 switch (request.cachePolicy) { case NSURLRequestUseProtocolCachePolicy: case NSURLRequestReturnCacheDataElseLoad: case NSURLRequestReturnCacheDataDontLoad: { UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil]; //如果发现了缓存,则直接调用回调并返回 if (cachedImage != nil) { if (success) { dispatch_async(dispatch_get_main_queue(), ^{ success(request, nil, cachedImage); }); } return; } break; } default: break; } //获取磁盘缓存的操作在这里在这里!!!!!! NSCachedURLResponse *response = [self.sessionManager.session.configuration.URLCache cachedResponseForRequest:request]; if (response) { UIImage *image = [UIImage imageWithData:response.data]; dispatch_async(dispatch_get_main_queue(), ^{ success(nil, nil, image); }); } //4. 创建资源请求,设置授权,检查可用性以及结果序列化解析器 NSUUID *mergedTaskIdentifier = [NSUUID UUID]; NSURLSessionDataTask *createdTask; __weak __typeof__(self) weakSelf = self; ... ... ... }); if (task) { return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task]; } else { return nil; } }
只是需要注意:使用磁盘缓存不能直接返回结果,因为有可能该资源链接对应的资源已经更新了,如果在不清理缓存的前提下直接返回就会导致,该资源永远无法更新.我们在这里使用磁盘缓存其实想当一个展位的图片,当获取到最终的图片资源时还是会进行更新.