总览
UIImagView+AFNetworking是我们常用的一个扩展,用它一行代码就可以实现图片的加载和自动缓存,非常的方便。下面我们来看下它是怎么实现的:
首先来看它的类图:
其主要由以上几个类和protocol组成。下面我们大致说一下各个类的作用:
- UIimageView+AFNetworking
这个类是我们使用的扩展类,里面主要是调度了图片的加载相关操作,比如查找缓存以及下载图片。 - AFImageDownloader
这个类是真正的下载管理类,下载使用AFHTTPSessionManager来进行。每个下载以task的方式运行。 - AFAutoPurgingImageCache
这个类是缓存类,里面管理了整个缓存内容。 - AFCachedImage
这个类是图片数据类,对图片数据和图片标示进行包装。 - AFImageDownloadReceipt
这个类主要用来做任务的取消。 - AFImageDownloaderMergedTask
这个类是下载任务类,封装了任务相关信息。 - AFImageDownloaderResponseHandler
这个类回调类,主要封装了一个成功和一个失败回调。
执行过程
1.首先在UIImageView+AFNetworking中,利用setImage系列方法来加载图片:
- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url
placeholderImage:(nullable UIImage *)placeholderImage;
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(nullable UIImage *)placeholderImage
success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
前两个方法最终其实是调用第三个方法,所以我们直接看第三个方法。这个方法首先会做一些必要的检查工作,之后会去调用AFImageRequestCache类查找本地是否已经有了此缓存。如果找到直接返回,如果找不到,就去服务端请求图片。
2.缓存是存在AFImageDownloader中的imageChche属性里面。找缓存API为:
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
在这个方法里面,会根据真正存储图片的类AFCachedImage里面的identifier来确定是否是需要的图片,identifier是由url加一个additionalIdentifier组成。其拼接为一个完成key的方法为:
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}
3.如果没找到缓存,就会去服务端下载图片。在AFImageDownloader中有两个方法提供下载功能:
- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
withReceiptID:(NSUUID *)receiptID
success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
第一个方法会去调第二个方法。在这个方法中,首先会去做一些基础的检查工作,之后会去检查要请求的图片是否已在任务中,所有任务都放在AFImageDownloader的mergedTasks字典属性中,key是url。如果发现任务已存在,就在任务中多添加一个回调,回调放在AFImageDownloaderResponseHandler类中。如果没有找到则根据初始设定的缓存规则来确定是不是需要读取缓存。
// 2) Attempt to load the image from the image cache if the cache policy allows it
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;
}
这里一共有三种缓存策略可以使用缓存:
- NSURLRequestUseProtocolCachePolicy。 对特定的 URL 请求使用网络协议中实现的缓存逻辑
- NSURLRequestReturnCacheDataElseLoad。无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
- NSURLRequestReturnCacheDataDontLoad。 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败。
如果不允许使用缓存或者没找到缓存,就要走正常的请求流程,真正的去请求数据了。请求到的数据好会添加到缓存中,以供下次使用。
请求及缓存处理中的多线程
首先来看请求相关部分:
NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
请求队列synchronizationQueue是一个串行队列,这是因为请求处理需要做一些任务检查和添加操作,而这些操作都是属于共享的资源(这里的共享资源主要指activeRequestCount、mergedTasks等)。如果使用并行队列的话,需要对每个共享资源加锁,而这里的几个共享资源都是逻辑上相关的,所以很容易造成死锁,所以这里使用串行队列可以省很多麻烦。 关于使用串行队列来避免加锁我们可以举一个例子来看一下:
- (void)safelyDecrementActiveTaskCount {
dispatch_sync(self.synchronizationQueue, ^{
if (self.activeRequestCount > 0) {
self.activeRequestCount -= 1;
}
});
}
这里将活跃请求的数量的递减放到了串行队列中,如果想通过队列显示数量的增减,那数量的增加必然也要放到同一个串行队列中,才可以在不用锁的情况下保证资源的正确性。所以有了下面的代码:
- (void)safelyStartNextTaskIfNecessary {
dispatch_sync(self.synchronizationQueue, ^{
if ([self isActiveRequestCountBelowMaximumLimit]) {
while (self.queuedMergedTasks.count > 0) {
AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];
if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {
[self startMergedTask:mergedTask];
break;
}
}
}
});
}
这里,资源的增加同样放到了synchronizationQueue队列中执行。这样就保证了共享资源的安全性。
返回队列responseQueue是一个并行队列。这是由于返回队列的一个重要作用就是快速分发返回结果,而这些并没有对共享资源做出修改,这样可以保证最快速的分发。
除了请求相关内容,在缓存管理上,也有一些值得说道的地方:
NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
这里缓存的管理是创建了一个并行队列synchronizationQueue。缓存的读取用了这样 一个方法:
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
image = [cachedImage accessImage];
});
return image;
}
缓存的增删代码如下:
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});
dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];
UInt64 bytesPurged = 0;
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}
通过上面的一系列方法可以看到写入用了dispatch_barrier_sync和dispatch_barrier_async来保证执行的一致性。对于读取,使用了dispatch_sync来实现满足同时支持多个读取操作,也就是单一资源的多读单写(具体在这儿有介绍)。我想这也是cache操作使用并行而不是串行队列的原因。
参考
iOS - 关于NSURLCache
NSURLRequestCachePolicy—iOS缓存策略
底层并发 API
AFNetworking之UIKit扩展与缓存实现