前言
不知不觉,我们已经通过前面两篇文章的介绍(我是链接=;=),对 SDWebImage 的工作流程有了较为清晰的认识,那么,今天就让我们把重点放在 SDWebImageDownloader
上,它到底做了哪些工作?又有哪些奇淫技巧?别着急,你慢慢往下看:)
SDWebImageDownloaderOptions
在切入正题前,我们有必要来了解下 SDWebImageDownloaderOptions
。
在下载的过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举 SDWebImageDownloaderOptions
定义,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { SDWebImageDownloaderLowPriority = 1 << 0, SDWebImageDownloaderProgressiveDownload = 1 << 1, // 默认情况下请求不使用 NSURLCache,如果设置该选项,则以默认的缓存策略来使用 NSURLCache SDWebImageDownloaderUseNSURLCache = 1 << 2, // 如果从 NSURLCache 缓存中读取图片,则使用 nil 作为参数来调用图片下载完成时 block SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, // 在 iOS 4+ 系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消 SDWebImageDownloaderContinueInBackground = 1 << 4, // 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES 来处理存储在 NSHTTPCookieStore 中的 cookie SDWebImageDownloaderHandleCookies = 1 << 5, // 允许不受信任的 SSL 证书,主要用于测试目的 SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, // 将图片下载放到高优先级队列中 SDWebImageDownloaderHighPriority = 1 << 7, }; |
SDWebImage 的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示:
1 2 3 4 5 6 | typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { // 以队列的方式,按照先进先出的顺序下载,这是默认的下载顺序 SDWebImageDownloaderFIFOExecutionOrder, // 以栈的方式,按照后进先出的顺序下载 SDWebImageDownloaderLIFOExecutionOrder }; |
NSOperation & NSOperationQueue
什么?还不让我看下载的具体代码!!!
嘻嘻,不要急嘛,俗话说「心急吃不了热豆腐」,再容我向你介绍下 NSOperation
与 NSOperationQueue
:
-
NSOperation
是一个抽象类,你可以用它来封装一个任务的相关代码和数据。因为它是个抽象类,所以你不能直接使用它,而是需要继承并实现其子类或者使用系统内置的两个子类(NSInvocationOperation
和NSBlockOperation
)来执行实际的线程任务 -
NSOperationQueue
类管理着一组NSOperation
对象的执行,当一个 operation 对象被加入到队列后,它会始终保留在队列中,直到它已经明确的被取消或者完成执行任务。Operations 在队列内(但尚未执行),它们是根据优先级和互相依赖进行组织的,相应的去执行。一个应用可以创建多个操作队列(operation queues)并提交操作(operations)到其中任何一个中
SDWebImageDownloader 下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个 NSOperationQueue
操作队列中来完成的,其声明如下:
1
| @property (strong, nonatomic) NSOperationQueue *downloadQueue;
|
默认情况下,队列最大并发数是 6。如果需要的话,我们可以通过 SDWebImageDownloader
类的 maxConcurrentDownloads
属性来修改。
所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的,其声明及定义如下:
1 2 3 4 5 6 7 8 9 | @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; - (id)init { if ((self = [super init])) { ... _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); ... } return self; } |
downloadImageWithURL:
哇靠,又是 downloadImageWithURL:
,这是什么鬼?
整个下载管理器对于下载请求的管理都是放在 downloadImageWithURL:options:progress:completed:
方法里面来处理的,而该方法又调用了 addProgressCallback:andCompletedBlock:forURL:createCallback:
方法来将请求的信息存入管理器中,同时在创建回调的 block 中创建新的操作,配置之后将其放入 downloadQueue 操作队列中,最后方法返回新创建的操作,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... // 创建请求对象,并根据 options 参数设置其属性 // 为了避免潜在的重复缓存(NSURLCache + SDImageCache),如果没有明确告知需要缓存,则禁用图片请求的缓存操作 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; ... // 创建 SDWebImageDownloaderOperation 操作对象,并进行配置 // 配置信息包括是否需要认证、优先级 operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { // 从管理器的 callbacksForURL 中找出该 URL 所有的进度处理回调并调用 ... for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { // 从管理器的 callbacksForURL 中找出该 URL 所有的完成处理回调并调用 // 如果 finished 为 YES,则将该 url 对应的回调信息从 URLCallbacks 中删除 ... if (finished) { [sself removeCallbacksForURL:url]; } for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } } cancelled:^{ // 取消操作将该 url 对应的回调信息从 URLCallbacks 中删除 SDWebImageDownloader *sself = wself; if (!sself) return; [sself removeCallbacksForURL:url]; }]; ... // 将操作加入到操作队列 downloadQueue 中 // 如果是 LIFO 顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作 [wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; |
addProgressCallback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } return; } // 以 dispatch_barrier_sync 操作来保证同一时间只有一个线程能对 URLCallbacks 进行操作 dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // 处理同一 URL 的同步下载请求的单个下载 NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; if (first) { createCallback(); } }); } |
方法会先查看这个 url 是否有对应的 callback,使用的是 downloader 持有的一个字典 URLCallbacks
。
如果是第一次添加回调的话,就会执行 first = YES,这个赋值非常的关键,因为 first 不为 YES 那么 HTTP 请求就不会被初始化,图片也无法被获取。
然后,在这个方法中会重新修正在 URLCallbacks
中存储的回调块。
如果是第一次添加回调块,那么就会直接运行这个 createCallback 这个 block,而这个 block,就是我们在前一个方法 downloadImageWithURL:options:progress:completed:
中传入的回调块:
1 2 3 | // SDWebImageDownloader // downloadImageWithURL:options:progress:completed: #4 [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }]; |
SDWebImageDownloaderOperation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | - (void)start { @synchronized (self) { // 管理下载状态,如果已取消,则重置当前下载并设置完成状态为 YES if (self.isCancelled) { self.finished = YES; [self reset]; return; } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; // 开启后台下载 if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { __weak __typeof__ (self) wself = self; UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ __strong __typeof (wself) sself = wself; if (sself) { [sself cancel]; [app endBackgroundTask:sself.backgroundTaskId]; sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; } #endif self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread]; } [self.connection start]; if (self.connection) { if (self.progressBlock) { self.progressBlock(0, NSURLResponseUnknownLength); } // 在主线程发通知 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); } else { // 在默认模式下运行当前 Run Loop,直到调用 CFRunLoopStop 停止运行 CFRunLoopRun(); } if (!self.isFinished) { [self.connection cancel]; [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; } } else { if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES); } } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } if (self.backgroundTaskId != UIBackgroundTaskInvalid) { UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; [app endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } #endif } |
这个类就是处理 HTTP 请求,URL 连接的类,当这个类的实例被加入队列之后,start
方法就会被调用, 而 start
方法首先就会产生一个 NSURLConnection
。
Update:
SDWebImage 在版本 3.8.0 时将 NSURLConnection
替换成了 NSURLSession
(大势所趋啊~)。但本文是以 NSURLConnection
为例进行讲解,如果你对 NSURLConnection
不了解,可以先看下我的文章:从 NSURLConnection 到 NSURLSession
———————————————华丽的分割线———————————————
在 start
方法调用之后,就是 NSURLConnectionDataDelegate
中代理方法的调用:
1 2 3 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection; |
在这三个代理方法中的前两个会不停回调 progressBlock
来提示下载的进度。
而最后一个代理方法会在图片下载完成之后调用 completionBlock
来完成最后 UIImageView.image 的更新。
而这里调用的 progressBlock
、completionBlock
、cancelBlock
都是在之前存储在 URLCallbacks
字典中的。
didReceiveData:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { // 附加数据 [self.imageData appendData:data]; if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { // 获取已下载数据总大小 const NSInteger totalSize = self.imageData.length; // 更新数据源,我们需要传入所有数据,而不仅仅是新数据 CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); // 首次获取到数据时,从这些数据中获取图片的长、宽、方向属性值 if (width + height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { NSInteger orientationValue = -1; CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &width); val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); CFRelease(properties); // 当绘制到 Core Graphics 时,我们会丢失方向信息,这意味着有时候由initWithCGIImage 创建的图片的方向会不对,所以在这边我们先保存这个信息并在后面使用 orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } } // 图片还未下载完成 if (width + height > 0 && totalSize < self.expectedSize) { // 使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张 CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE // 适用于 iOS 变形图像的解决方案。我的理解是由于 iOS 只支持 RGB 颜色空间,所以在此对下载下来的图片做个颜色空间转换处理 if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif // 对图片进行缩放、解码操作 if (partialImageRef) { UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; UIImage *scaledImage = [self scaledImageForKey:key image:image]; if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; } CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } CFRelease(imageSource); } if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); } } |
该方法的主要任务是接收数据。每次接收到数据时,都会用现有的数据创建一个 CGImageSourceRef 对象以做处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用 CGImageSourceRef 对象创建一个图片对象,经过缩放、解压缩操作后生成一个 UIImage 对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
要点
- 通知的接收所在的线程是基于发送通知所在的线程,如果通知是在主线程发出的,通知的接收也是在主线程,如果通知的发送是在子线程,通知的接收也是在子线程。(如果想回主线程,可使用
dispatch_async(dispatch_get_main_queue(), ^
)
关于 SDWebImage 的整体分析就到这里啦,我会在终结篇:SDWebImage 源码阅读笔记(四)中对某些知识点进行扩展,感兴趣的同学不妨去瞅瞅呗!
原文:http://itangqi.me/2016/03/23/the-notes-of-learning-sdwebimage-three/