下载实现
在上篇文章中,我们对AFN的缓存策略实现做了解析,今天我们来看一下AFN如何实现下载图片.
2.1 AFImageDownloadReceipt
跟其他的图片加载类库相似,AFN对每个请求进行了封装,使用这个类我们进行了对请求进行操作.在AFN中这个类就是AFImageDownloadReceipt,每一个发起的请求都会对应这样的对象.这个对象是有两个属性,
/** The data task created by the `AFImageDownloader`. */ @property (nonatomic, strong) NSURLSessionDataTask *task; /** The unique identifier for the success and failure blocks when duplicate requests are made. */ @property (nonatomic, strong) NSUUID *receiptID;
- task:由AFImageDownloader创建的数据请求任务,必要时可以对这个任务进行相关处理,比如cancel等;
- receiptID:主要用于取消请求操作.
2.2 AFImageDownloaderResponseHandler
AFImageDownloaderResponseHandler这个类在AFN中用来封装请求的回调保存.这个类中,有三个属性:
@property (nonatomic, strong) NSUUID *uuid; @property (nonatomic, copy) void (^successBlock)(NSURLRequest*, NSHTTPURLResponse*, UIImage*); @property (nonatomic, copy) void (^failureBlock)(NSURLRequest*, NSHTTPURLResponse*, NSError*);
- uuid:这个作为请求的唯一标记,与AFImageDownloadReceipt中的receiptID保持一致,用来唯一标识特定请求。通过这个标志可以把请求和回调进行一一对应;
- successBlock:请求的成功回调;
- failureBlock:请求的失败回调.
2.3 AFImageDownloaderMergedTask
AFImageDownloaderMergedTask该类主要用于合并针对同一资源的发起的重复请求,避免资源浪费.
//资源字符串 @property (nonatomic, strong) NSString *URLIdentifier; //NSUUID字符串,用于结果返回时匹配对应的请求,方便调用回调结果 @property (nonatomic, strong) NSUUID *identifier; //资源字符串对应的数据请求任务 @property (nonatomic, strong) NSURLSessionDataTask *task; //URLIdentifier对应的多个请求 @property (nonatomic, strong) NSMutableArray <AFImageDownloaderResponseHandler*> *responseHandlers;
- URLIdentifier:每个请求发起时均会产生一个被封装了回调结果mergedTask(AFImageDownloaderMergedTask对象),然后被保存在全局mergedTasks中.当新的请求request准备发起时,会使用URLIdentifier与request.URL.absoluteString做比较:判断准备发起的请求与已经存在的mergedTask是否指向同一个资源,如果是则合并该回调结果对象到已经存在的mergedTask的responseHandlers属性中,不存在则重新创建并保存在全局的mergedTasks;
- identifier:任务的标志,等待收到结果时匹配具体任务,然后调用保存的回调结果;
- task:发起该资源请求的任务对象;
- responseHandlers:URLIdentifier对应资源的回调集合,用于获取到结果时进行遍历回调.
??这里或许有人会有疑问,既然为什么有了URLIdentifier作为标志符号来区分请求,为什么还需要一个identifier??
其实这里作者做了一个更细致化的处理.我们知道其实每个请求resuest都会生成一个handler(AFImageDownloaderResponseHandler对象),同时生成一个receipt(AFImageDownloadReceipt对象),所以他们有一个共同的标志,那就是uuid.而对于同一资源的request来讲,是mergedTask(AFImageDownloaderMergedTask对象)将他们链接起来,并将同一个资源请求下的回调全部保存在mergedTask中.那么问题来了:如果在某种需求下,我对同一链接资源发送了多次请求,但是只想要取消某个请求,另外两次的请求我并不想取消怎么办呢?我们根据URLIdentifier只能获取到mergedTask,但是mergedTask保存了这三个请求的回调,所以只时候就需要区分一下请求的回调,来确定哪个回调需要提前取消掉的,而不用等待请求结果.
2.4 AFImageDownloader
AFImageDownloader是AFN可以直接用于加载图片资源的实现类.
//任务串行队类 @property (nonatomic, strong) dispatch_queue_t synchronizationQueue; //结果回调队列 @property (nonatomic, strong) dispatch_queue_t responseQueue; //最大活跃任务数 @property (nonatomic, assign) NSInteger maximumActiveDownloads; //当前活跃任务数 @property (nonatomic, assign) NSInteger activeRequestCount; //排队中的任务数组(activeRequestCount > maximumActiveDownloads)任务需要排队 @property (nonatomic, strong) NSMutableArray *queuedMergedTasks; //保存请求合并回调回调对象,用于结果回调 @property (nonatomic, strong) NSMutableDictionary *mergedTasks;
- synchronizationQueue:全局串行队里,用于执行依赖任务;
- responseQueue:全局并发队列,用于高效执行结果回调;
- maximumActiveDownloads:允许的最大并发下载数量.在AFN中设定最大并发数量为4;
- activeRequestCount:当前正在并发的下载任务数量;
- queuedMergedTasks:当需要下载的任务超过maximumActiveDownloads时,未能执行的任务需要保存在该数组中进行排队;
- mergedTasks:用于保存请求对应的合并回调对象.当获取到请求结果时,通过资源链接获取到保存在该数组中的对应的AFImageDownloaderMergedTask对象进行结果回调.
2.4.1 defaultURLCache
+ (NSURLCache *)defaultURLCache { // It's been discovered that a crash will occur on certain versions // of iOS if you customize the cache. // // More info can be found here: https://devforums.apple.com/message/1102182#1102182 // // When iOS 7 support is dropped, this should be modified to use // NSProcessInfo methods instead. if ([[[UIDevice currentDevice] systemVersion] compare:@"8.2" options:NSNumericSearch] == NSOrderedAscending) { return [NSURLCache sharedURLCache]; } //默认设定内存缓存为20M,硬盘缓存为150M return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024 diskCapacity:150 * 1024 * 1024 diskPath:@"com.alamofire.imagedownloader"]; }
AFImageDownloader使用了系统NSURLCache进行请求的缓存,这也是AFN曾经和SD撕扯的一个技术点,该类会对请求进行本地数据的持久化,文章最后我们再做统一说明.而之所以要区分系统个是因为在低版本的系统上使用自定的NSURLCache会有闪退,当iOS 7的支持被摒弃之后,会使用NSProcessInfo来进行替换.
2.4.2 defaultURLSessionConfiguration
+ (NSURLSessionConfiguration *)defaultURLSessionConfiguration { NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; //TODO set the default HTTP headers //允许设置cookie信息 configuration.HTTPShouldSetCookies = YES; //不允许http请求在没收到回复之前再次发情请求:由于http请求没有状态,所以客户端依赖发起顺序处理结果 configuration.HTTPShouldUsePipelining = NO; //设置缓存策略 configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy; //允许通过CellularAccess发起链接请求 configuration.allowsCellularAccess = YES; //设置超时时间 configuration.timeoutIntervalForRequest = 60.0; //使用自定义NSURLCache缓存 configuration.URLCache = [AFImageDownloader defaultURLCache]; return configuration; }
该方法用于生成默认使用的NSURLSessionConfiguration对象.
2.4.3 init
//AFImageDownloader默认的单例实现 + (instancetype)defaultInstance { static AFImageDownloader *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } //AFImageDownloader默认的init实现 - (instancetype)init { NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration]; return [self initWithSessionConfiguration:defaultConfiguration]; } //接收外界实现自定义NSURLSessionConfiguration - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration { AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration]; sessionManager.responseSerializer = [AFImageResponseSerializer serializer]; return [self initWithSessionManager:sessionManager downloadPrioritization:AFImageDownloadPrioritizationFIFO maximumActiveDownloads:4 imageCache:[[AFAutoPurgingImageCache alloc] init]]; } //接收外界提供更多的自定义选项 - (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization maximumActiveDownloads:(NSInteger)maximumActiveDownloads imageCache:(id <AFImageRequestCache>)imageCache { if (self = [super init]) { self.sessionManager = sessionManager; self.downloadPrioritizaton = downloadPrioritization; self.maximumActiveDownloads = maximumActiveDownloads; self.imageCache = imageCache; self.queuedMergedTasks = [[NSMutableArray alloc] init]; self.mergedTasks = [[NSMutableDictionary alloc] init]; self.activeRequestCount = 0; 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); } return self; }
AFImageDownloader提供了系统默认的init实现,同时开放了给多的自定义实现接口给用户使用.
2.4.4 downloadImageForURLRequest:withReceiptID:success:failure:
我们将该方法拆分为两部分,发起请求之前的处理和获取到请求结果之后的处理.
- (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 { // 1.判断当前资源链接是否为空 __block NSURLSessionDataTask *task = nil; dispatch_sync(self.synchronizationQueue, ^{ NSString *URLIdentifier = request.URL.absoluteString; if (URLIdentifier == nil) { if (failure) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil]; dispatch_async(dispatch_get_main_queue(), ^{ failure(request, nil, error); }); } return; } // 2. 如果资源链接对应的AFImageDownloaderMergedTask已经存在(说明该链接对应的资源请求已经发出但是还没有收到请求结果)则将该回调添加到该对象中,同时直接返回,不再发起重复的请求 AFImageDownloaderMergedTask *existingMergedTask = self.mergedTasks[URLIdentifier]; if (existingMergedTask != nil) { AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure]; [existingMergedTask addResponseHandler:handler]; task = existingMergedTask.task; return; } //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; } //4. 创建资源请求,设置授权,检查可用性以及结果序列化解析器 NSUUID *mergedTaskIdentifier = [NSUUID UUID]; NSURLSessionDataTask *createdTask; __weak __typeof__(self) weakSelf = self; createdTask = [self.sessionManager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { //这块代码我们单独分析 }]; // 5. 存储response handler用户请求完成时回调 AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure]; AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc] initWithURLIdentifier:URLIdentifier identifier:mergedTaskIdentifier task:createdTask]; [mergedTask addResponseHandler:handler]; self.mergedTasks[URLIdentifier] = mergedTask; // 6. 根据当前的活跃请求数,决定发起请求或者让请求排队 if ([self isActiveRequestCountBelowMaximumLimit]) { [self startMergedTask:mergedTask]; } else { [self enqueueMergedTask:mergedTask]; } task = mergedTask.task; }); if (task) { return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task]; } else { return nil; } }
这个是实现资源加载的主要方法:
- 根据请求URLIdentifier判断该资源链接是否为空,如果为空则直接返回;
- 根据URLIdentifier判断在mergedTasks是否已经存在对应的mergedTask对象,如果存在则直接将reponse handler添加到该对象mergedTask中,直接返回,不再针对同一资源发起重复请求;
- 根据request的cachePolicy策略来判断是否要从缓存中查找资源:如果当前请求的缓存策略允许从内存中获取则直接从内存中获取,并返回;
- 经过上述条件过滤,则证明需要需要发起资源请求:创建资源请求,设置授权,检查可用性以及结果序列化解析器;
- 创建mergedTask,并将当前请求对应的reponse handler添加到mergedTask,保存至全局mergedTasks对象;
- 处理当前mergedTask:根据activeRequestCount是否小于maximumActiveDownloads来确定是直接发起资源请求还是让请求暂时排队.其中排队中的任务会在请求完成之后重新开始.
当收到请求结果之后:
dispatch_async(self.responseQueue, ^{ //遍历mergedTask并执行response handler回调结果 __strong __typeof__(weakSelf) strongSelf = weakSelf; AFImageDownloaderMergedTask *mergedTask = strongSelf.mergedTasks[URLIdentifier]; if ([mergedTask.identifier isEqual:mergedTaskIdentifier]) { //移除当前的URLIdentifier对应的mergedTask对象 mergedTask = [strongSelf safelyRemoveMergedTaskWithURLIdentifier:URLIdentifier]; if (error) { //失败回调 for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) { if (handler.failureBlock) { dispatch_async(dispatch_get_main_queue(), ^{ handler.failureBlock(request, (NSHTTPURLResponse*)response, error); }); } } } else { //缓存获取到的图片 if ([strongSelf.imageCache shouldCacheImage:responseObject forRequest:request withAdditionalIdentifier:nil]) { [strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil]; } //成功回调 for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) { if (handler.successBlock) { dispatch_async(dispatch_get_main_queue(), ^{ handler.successBlock(request, (NSHTTPURLResponse*)response, responseObject); }); } } } } //当前活跃请求数activeRequestCount减一 [strongSelf safelyDecrementActiveTaskCount]; //如果有请求在排队则开启新的 [strongSelf safelyStartNextTaskIfNecessary]; });
收到请求结果之后:
- 根据URLIdentifier获取到对应的mergedTask,mergedTask保存了针对该URLIdentifier的所有请求回调,同时删除mergedTasks中的mergedTask引用;
- 请求出现错误:如果请求过程总出现错误,则遍历mergedTask中保存的失败回调,并逐个调用;
- 正常返回请求资源:如果请求过程中无异常,首先判断是否需要缓存图片:根据shouldCacheImage:forRequest:withAdditionalIdentifier:确定该资源是否需要缓存(默认实现需要缓存);然后遍历mergedTask中保存的成功回调,并逐个调用;
- 将当前活跃的activeRequestCount减一,并唤醒一个新的排队任务.
2.4.5 cancelTaskForImageDownloadReceipt:
/** 取消指定请求 @param imageDownloadReceipt 请求对应的AFImageDownloadReceipt对象 */ - (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt { dispatch_sync(self.synchronizationQueue, ^{ NSString *URLIdentifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString; //1. 获取到对应的AFImageDownloaderMergedTask对象 AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier]; //2. 获取到对应的handler在mergedTask中的index NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) { return handler.uuid == imageDownloadReceipt.receiptID; }]; if (index != NSNotFound) { /* 如果查询到对应的handler: 1. 获取到handler的引用; 2. 从mergedTask移除该回调; 3. 执行h该回调 */ AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index]; [mergedTask removeResponseHandler:handler]; NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString]; NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason}; NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo]; if (handler.failureBlock) { dispatch_async(dispatch_get_main_queue(), ^{ handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error); }); } } //如果对应的mergedTask已经没有其他回调,且对应的task尚未开始执行则取消该任务,并从排队队列中移除 if (mergedTask.responseHandlers.count == 0 && mergedTask.task.state == NSURLSessionTaskStateSuspended) { [mergedTask.task cancel]; [self removeMergedTaskWithURLIdentifier:URLIdentifier]; } }); }
这个方法主要用来处理取消任务:
- 根据URLIdentifier获取到对应的mergedTask对象;
- 获取该imageDownloadReceipt对应的handler回调在mergedTask.responseHandlers索引;
- 如果获取索引成功:首先获取索引到对应的handler引用;然后从mergedTask删除该handler引用;最后执行该handler失败回调.