【原】SDWebImage源码阅读(三)
【原】SDWebImage源码阅读(三)
本文转载请注明出处 —— polobymulberry-博客园
1.SDWebImageDownloader中的downloadImageWithURL
我们来到SDWebImageDownloader.m文件中,找到downloadImageWithURL函数。发现代码不是很长,那就一行行读。毕竟这个函数大概做什么我们是知道的。这个函数大概就是创建了一个SDWebImageSownloader的异步下载器,根据给定的URL下载image。
先映入眼帘的是下面两行代码,简单地开开胃:
// 封装了异步下载图片操作 __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self;
接着又是一个函数直接到底:addProgressCallback。这是SDWebImageDownloader的私有函数,所以直接一点点看它实现。
// 这里的url不能为空,下面会解释。如果为空,completedBlock中image、data和error直接传入nil if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } return; }
之所以url不能为空,是因为这个url要作为NSDictionary变量的key值,所以不能为空。而这个NSDictionary变量就是URLCallbacks。我们从名称大概可以猜到,这个NSDictionary应该是存储每个url对应的callback(本质是因为一个url基本上对应一个网络请求,而每个网络请求就是一个SDWebImageDownloaderOperation,而这个SDWebImageDownloaderOperation初始化是使用initWithRequest进行的,initWithRequest需要提供这些callbacks)。那对应的callback函数都有哪些呢?
我们先找到URLCallbacks的赋值语句:
self.URLCallbacks[url] = callbacksForURL;
那callbacksForURL又是什么?看上面
NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks];
注意到callbacksForURL是一个NSMutableArray类型,那它其中对应的每个object存储的是什么呢?看addObject:callbacks,原来是callbacks。那callbacks又是什么?居然是一个NSMutableDictionary类型。而且存储了对应的progressBlock和completedBlock。这下我们就明白了其中的关系,如图:
这个函数还有一处要注意,就是如果当前url是第一次请求,也就是说对应的URLCallbacks[url]为空,那就新建一个,同时置first为YES,就是说这是第一次创建该url的callbacks。而且还会调用createCallback,相当于第一次初始化过程。
另外整个代码是放在下面的dispatch_barrier_sync中:
dispatch_barrier_sync(self.barrierQueue, ^{ //... });
因为此函数可能会有多个线程同时执行(因为允许多个图片的同时下载),那么就有可能会有多个线程同时修改URLCallbacks,所以使用dispatch_barrier_sync来保证同一时间只有一个线程在访问URLCallbacks。并且此处使用了一个单独的queue--barrierQueue,并且这个queue是一个DISPATCH_QUEUE_CONCURRENT类型的。也就是说,这里虽然允许你针对URLCallbacks的操作是并发执行的,但是因为使用了dispatch_barrier_sync,所以你必须保证之前针对URLCallbacks的操作要完成才能执行下面针对URLCallbacks的操作。
注意:我发现使用barrierQueue的都是dispatch_barrier_sync、dispatch_barrier_async、dispatch_sync,我就纳闷了,这些有用到并发的东西吗?为什么不直接使用DISPATCH_QUEUE_SERIAL。求大神告知!下面讨论区一楼和二楼有具体讨论。
总的来说,上面那个addProgressCallback函数主要就是生成了每个url的callbacks,并且以URLCallbacks形式传递给别人。具体我们回到downloadImageWithURL中再看。
回到downloadImageWithURL函数中的addProgressCallback中,看到它具体的createCallback实现。代码不是很长。也是按顺序看:
NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; }
downloadTimeOut表示的下载超时的限定时间,默认是15秒。
然后再往下看就傻眼了,之前对iOS的网络部分一窍不通啊。没办法,硬着头皮,一点点死扣吧。
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
首先要知道initWithURL函数是做什么的?看看注释,大概明白了。就是根据url,缓存策略(cachePolicy)和超时限定时间(timeoutInterval)来产生一个NSURLRequest。这里比较麻烦的是cachePolicy,就是告诉这个request(请求)如何缓存结果:
(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData)
- SDWebImageDownloaderUseNSURLCache:在SDWebImage中,缺省情况下,request是不使用NSURLCache的,但是若使用该选项,就默认使用NSURLCache默认的缓存策略:NSURLRequestUseProtocolCachePolicy。
- NSURLRequestUseProtocolCachePolicy:对特定的 URL 请求使用网络协议(如HTTP)中实现的缓存逻辑。这是默认的策略。该策略表示如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断 下一步操作,如: Cache-Control字段为must-revalidata, 则 询问服务端该数据是否有更新,无更新话 直接返回给用户缓存数据,若已更新,则请求服务端.
- NSURLRequestReloadIgnoringLocalCacheData:数据需要从原始地址(一般就是重新从服务器获取)加载。不使用现有缓存。
接下来就是设置request的一些属性了(可以看出此处使用的实HTTP协议):
// 如果设置HTTPShouldHandleCookies为YES,就处理存储在NSHTTPCookieStore中的cookies。 // HTTPShouldHandleCookies表示是否应该给request设置cookie并随request一起发送出去。 request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); // HTTPShouldUsePipelining表示receiver(理解为iOS客户端)的下一个信息是否必须等到上一个请求回复才能发送。 // 如果为YES表示可以,NO表示必须等receiver收到先前的回复才能发送下个信息。 request.HTTPShouldUsePipelining = YES; // 如果你设置了SDWebImageDownloader的headersFilter,就是用你自定义的方法,来设置HTTP的header field。 // 如果没有自定义,就是用SDWebImage提供的HTTPHeaders。 // 简单看下HTTPHeader的初始化部分(如果下载webp图片,需要的header不一样): // #ifdef SD_WEBP // _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; // #else // _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy]; // #endif if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; }
有了NSURLRequest,接着使用了initWithRequest来初始化一个operation。细节暂且不看,直接跳过,后面的看完再来好好研究。先看下面:
operation.shouldDecompressImages = wself.shouldDecompressImages;
这个简单,就是说要不要解压缩图片。解压缩已经下载的图片或者在缓存中的图片,可以提高性能,但是会耗费很多空间,缺省情况下是要解压缩图片。
if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; }
urlCredential是一个NSURLCredential类型。
知识点:NSURLCredential
web 服务可以在返回 http 响应时附带认证要求的challenge,作用是询问 http 请求的发起方是谁,这时发起方应提供正确的用户名和密码(即认证信息),然后 web 服务才会返回真正的 http 响应。
收到认证要求时,NSURLConnection 的委托对象会收到相应的消息并得到一个 NSURLAuthenticationChallenge 实例。该实例的发送方遵守 NSURLAuthenticationChallengeSender 协议。为了继续收到真实的数据,需要向该发送方向发回一个 NSURLCredential 实例。
如果已经有了credential,那就直接赋值。如果没有,就用用户名(username)和密码(password)新构建一个:
[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
其中NSURLCredentialPersistenceForSession表示在应用终止时,丢弃相应的 credential 。
接着是设置该operation的优先级,毕竟operation对应一个NSOperation。
if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; }
这个简单,就是优先级设定,一般来说,优先级越高,执行越早。
然后就是添加到NSOperationQueue中,这个downloadQueue一看就知道肯定是NSOperationQueue,代码如下:
[wself.downloadQueue addOperation:operation];
最后是处理operation的执行顺序:
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // 如果执行顺序为LIFO(last in first out,后进先出,栈结构) // 就将新添加的operation作为最后一个operation的依赖,就是说,要执行最后一个operation,必须先执行完新添加的operation,这就实现了栈结构。 [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; }
刚才说的都是对operation的一些属性设置。现在可以回到operation创建的那个函数initWithRequest中了。顺便提一句,initWithRequest是SDWebImageDownloaderOperation函数,所以前面[wself.operationClass]返回的是SDWebImageDownloaderOperation(不相信的话,请搜索setOperationClass)。这也是一个编程技巧,把Class类型作为属性存起来。
// 先看看这个函数声明和注释,返回的是SDWebImageDownloaderOperation。 // 参数需要request,不过这个上面的代码已经创建好了,而options使用的是downloadImageWithURL传入的options // 真正需要在传递给此函数的就剩下三个block了:progressBlock、completedBlock、cancelBlock - (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock;
先看progress:
progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; }); for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); } }
其中主要难点在下面这段代码:
dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; });
注意此处使用了同步方法dispatch_sync,也就是说,callbacksForURL这条赋值语句是放在barrierQueue线程执行的,而且此时会阻塞当前线程。我们之前提到过,barrierQueue是为了保证同一时刻只有一个线程对URLCallbacks进行操作。说实话,我不是很明白这里为什么要使用dispatch_sync,为什么不用dispatch_barrier_sync?希望大神可以告知原因。(此处我回头想了下,可能是因为对于同一个图片下载任务,会不停地调用progressBlock函数,这个callbacksForURL的赋值语句可能是在同一个图片下载任务的不同的线程(一个图片每次下载到新数据后调用progressblock)中执行的,但是你必须要保证前一部分数据下载任务完成,才能执行后一部分数据的下载任务,此处需要同步,所以使用dispatch_sync,此处单独使用一个barrierQueue,还可以防止dispatch_sync造成死锁)。
跟着的for循环就好理解了,直接从callbacks中索引到progressBlock,放入主线程中进行下载,当然,下载过程中肯定要知道已经下载了多少(receivedSize)和预期下载的大小(expectedSize)。因为这个block是不停调用,只要有新的数据到达就调用,直到下载完成,所以这两个参数还是必备的,判断是否下载完成。
下面的completedBlock:
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } }); for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } }
这里使用的是dispatch_barrier_sync。不同图片的下载任务会异步完成,所以必要保证之前其他图片下载完成,并执行完completedBlock内的对URLCallbacks的操作,才能接着运行。因为只要等之前的进程完成,并不需要关心之前的进程是不是同步执行,所以使用的是dispatch_barrier_sync。其他逻辑部分,很简单,就不赘述了。
最后是cancelBlock:
cancelled:^{ SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }
因为取消了,所以直接把url从URLCallbacks中移除。但是此处同步方案又是用dispatch_barrier_async。其实我觉得在同一个queue中,使用dispatch_barrier_async还是使用dispatch_barrier_sync并没有什么区别。因为都是要等之前的执行完成。(不过dispatch_barrier_async表示的是先等之前的执行完成,然后把该barrier放入queue中,而不是等待barrier中代码执行结束,而dispat_barrier_sync表示需要等待barrier中代码执行结束)。
2. 运行
之前这个系列的博客都是为了构造一个operation(NSOperation),并且也放到downloadQueue(NSOperationQueue)。但是我们还需要点火启动这个operation。
我们实现了NSOperation的子类,那么要让其运行起来,要么实现main(),要么实现start()。这里SDWebImageDownloaderOperation选择实现了start()。我们先一步步看看start()实现:
先是一个线程线程同步锁(以self作为互斥信号量):
@synchronized (self) { // ... }
此处到底写了什么代码,居然需要同步,而且还是以加锁的方式?
首先是判断当前这个SDWebImageDownloaderOperation是否取消了,如果取消了,即认为该任务已经完成,并且及时回收资源(即reset)。
这里简单介绍下NSOperation的三个重要的状态,如果你使用了NSOperation,就需要手动管理这三个重要的状态:
isExecuting
代表任务正在执行中isFinished
代表任务已经执行完成isCancelled
代表任务已经取消执行
if (self.isCancelled) { self.finished = YES; [self reset]; // 资源回收,资源全部置为nil,自动回收 return; }
然后是一段宏中的代码,这段代码主要是考虑到app进入后台发生的事,虽然代码很简单,但是有些技巧还是需要学习的:
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; } }]; }
因为要使用beginBackgroundTaskWithExpirationHandler,所以需要使用[UIApplication sharedApplication],因为是第三方库,所以需要使用NSClassFromString获取到UIApplication。这里需要提及的就是shouldContinueWhenAppEntersBackground,也就是说下载选项中需要设置SDWebImageDownloaderContinueInBackground。
注意beginBackgroundTaskWithExpirationHandler并不是意味着立即执行后台任务,它只是相当于注册了一个后台任务,函数后面的handler block表示程序在后台运行时间到了后,要运行的代码。这里,后台时间结束时,如果下载任务还在进行,就取消该任务,并且调用endBackgroundTask,以及置backgroundTaskId为UIBackgroundTaskInvalid。
注意此处取消任务的方法cancel是SDWebImageDownloaderOperation重新定义的。
- (void)cancel { @synchronized (self) { if (self.thread) { [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO]; } else { [self cancelInternal]; } } }
这里我比较奇怪为什么self.thread存在和不存在是两种取消方式,而且什么情况下self.thread会不存在呢?
具体看cancelInternalAndStop和cancelInternal代码,发现cancelInternalAndStop就多了一行代码:
CFRunLoopStop(CFRunLoopGetCurrent());
因为每个NSThread都会有一个CFRunLoop(后面的代码会有CFRunLoopRun函数出现),所以如果要取消的话,就得同时stop这个RunLoop。所以cancel函数的逻辑主要就是cancelIntenal函数了。
cancelIntenal函数所做了三件事:
- 1.调用自定义的cancelBlock。
- 2.调用NSURLConnection的cancel取消self.connection。
- 3.回收资源。
注意到在取消self.connection过程中,发送了一个SDWebImageDownloadStopNotification的通知。我们可以看到这个通知注册的地方是在SDWebImageDownloader类的initialize函数:
+ (void)initialize { // Bind SDNetworkActivityIndicator if available (download it here: http://github.com/rs/SDNetworkActivityIndicator ) // To use it, just add #import "SDNetworkActivityIndicator.h" in addition to the SDWebImage import if (NSClassFromString(@"SDNetworkActivityIndicator")) { // .... [[NSNotificationCenter defaultCenter] addObserver:activityIndicator selector:NSSelectorFromString(@"stopActivity") name:SDWebImageDownloadStopNotification object:nil]; } }
注意到如果你要使用这个SDWebImageDownloadStopNotification通知,需要绑定SDNetworkActivityIndicator,这个貌似是需要单独下载的。当然,你可以修改这部分源代码,换成别的ActivityIndicator。
这里就有疑问了,此时我们的backgroundTaskId已经注册过了,如果此NSOperation在进入后台运行之前就已经完成任务了,不就应该把这个backgroundTaskId置为UIBackgroundTaskInvalid吗,意思就是告诉系统,任务完成,不需要考虑进不进入后台运行的问题了。确实,在start函数末尾,就是判断如果下载任务完成(不管有没有下载成功),就将backgroundTaskId置为UIBackgroundTaskInvalid。
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; }
回到上面代码接着看:
self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread];
注册过后台代码后,接着就是要正式运行了。所以先要置executing属性为YES。然后就是关键的connection了。connection是一个NSURLConnection类型的属性。这里我们能感觉到,真正的下载图片的网络处理部分就是利用了NSURLConnection。此处使用的self.request就是上面提到的那个NSMutableURLRequest(在SDWebImageDownloader.m中的downloadImageWithURL函数中生成的)。其实我们现在应该看下SDWebImageDownloaderOperation中实现的NSURLConnectionDataDelegate方法。但是不急,先把start函数中的剩下函数看完。剩下的不是很难,所以先解决。
虽然已经使用init方法构建了一个NSURLConnection,但是真正要启动下载还需要使用NSURLConnection的start方法。
[self.connection start];
接下来就是判断这个connection是否创建成功:
if (self.connection) { // ...... } else { // ...... }
这个if else语句要分一下两个情形讨论:
情形1:connection创建成功
因为刚connection刚start,所以此处执行的progresBlock的参数为receivedSize=0,expectedSize=NSURLResponseUnknownLength(((long long)-1))。我们都知道一般除非自定义progressBlock,不然一般progresBlock为nil。所以如果这里用户自定义了progressBlock,但是这是用户定义的行为,为什么要将参数设置成这样呢?我不是很清楚,但是用户在设计自己的progressBlock的时候就要留心这个参数问题了,要特意处理expectedSize为NSURLResponseUnknownLength的情况。
接着回到主进程使用SDWebImageDownloadStartNotification,和之前说的SDWebImageDownloadStopNotification有异曲同工之处。读者可以自己查询。
接下来就是调用RunLoop了。这里它以NSFoundation的iOS5.1版本作为分界线进行讨论的,不过两者做的事情都一样,只不过调用函数不同罢了——都是调用RunLoop直到下载任务终止或者完成。
这是CFRunLoopRunInMode和CFRunLoopRun的源码:
CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }
CFRunLoopRun
void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); }
稍微提一下CFRunLoopRun,大概能看出来这是一个while循环,并且是在使用CFRunLoopGetCurrent()来不停地执行当前RunLoop的任务,直到任务被终止或者完成。
你可以这样理解这两个函数关系,CFRunLoopRun就是使用默认mode运行的CFRunLoopRunInMode。至于为什么iOS5.1之前的要使用CFRunLoopRunInMode,我们从其中的注释也可以看出,其实主要是利用CFRunLoopRunInMode的CFTimeInterval seconds参数。
那么执行当前进程的任务到底指什么?具体请看这篇文章--深入理解RunLoop。简单点说,这里进程主要是响应NSURLConnectionDataDelegate和NSURLConnectionDelegate的各种代理函数。
通常使用 NSURLConnection 时,你会传入一个 delegate,当调用了 [self.connection start] 后,这个delegate 就会不停收到事件回调。所以也就是说等这个connection完成或者终止,才会跳出CFRunLoopRun()。当跳出Runloop后,就要判断NSURLConnection是不是正常完成任务了。如果没有,也就是说self.isFinished == NO。那么就取消该connection,并且调用- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;返回错误信息,打印出错的请求url。总的代码如下:
if (!self.isFinished) { [self.connection cancel]; [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; }
情形2:connection创建失败
调用completedBlock。因为此处是失败了,所以image和data参数为nil,而error从它的NSLocalizedDescriptionKey就可以看出Connection can't be initialized。
3. SDWebImageManager中的downloadImageWithURL剩余部分
其实我们只剩下了SDWebImageDownloader的downloadImageWithURL中的completedBlock部分还没细说了。
completedBlock也分为三种情形:
3.1 情形1:operation(非subOperation)取消了
什么都不做。因为如果你要在此处调用completedBlock的话,可能会存在和其他的completedBlock产生条件竞争,可能会修改同一个数据。
if (weakOperation.isCancelled) { // ...... }
3.2 情形2:download产生了错误error
else if (error) { // ...... }
首先先判断operation是否取消了(检查是否取消要勤快点),没有取消,就调用completedBlock,处理error。
dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url); } });
随后检查错误类型,确认不是客户端或者服务器端的网络问题,就认为这个url本身问题了。并把这个url放到failedURLs中。
if ( error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code != NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code != NSURLErrorCannotConnectToHost) { @synchronized (self.failedURLs) { [self.failedURLs addObject:url]; } }
3.3 情形3
如果使用了SDWebImageRetryFailed选项,那么即使该url是failedURLs,也要从failedURLs移除,并继续执行download:
if ((options & SDWebImageRetryFailed)) { @synchronized (self.failedURLs) { [self.failedURLs removeObject:url]; } }
cacheOnDisk表示是否使用磁盘上的缓存:
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
接着又是一个if else。我们先大概看看框架:
// image是从SDImageCache中获取的,downloadImage是从网络端获取的 // 所以虽然options包含SDWebImageRefreshCached,需要刷新imageCached, // 并使用downloadImage,不过可惜downloadImage没有从网络端获取到图片。 if (options & SDWebImageRefreshCached && image && !downloadedImage) { // ...... } // 图片下载成功,获取到了downloadedImage。 // 这时候如果想transform已经下载的图片,就得先判断这个图片是不是animated image(动图), // 这里可以通过downloadedImage.images是不是为空判断。 // 默认情况下,动图是不允许transform的,不过如果options选项中有SDWebImageTransformAnimatedImage,也是允许transform的。 // 当然,静态图片不受此干扰。另外,要transform图片,还需要实现 // transformDownloadedImage这个方法,这个方法是在SDWebImageManagerDelegate代理定义的 else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { // ...... else { // 这个不用解释了 }
接着我们就可以具体看看每个判断里面的实现了:
- 首先是if,满足这种情况,就不需要调用completedBlock。
- 然后是else if,满足这种情况,首先肯定要将downloadedImage进行transform。
不过我们先看下transformDownloadedImage的注释:
// 允许在image刚下载完,以及在缓存到内存和disk之前,进行transform。 // 注意:该方法是在一个global queue中调用,为了避免阻塞主线程。
- 所以我们可以看到整个else if中的语句是包含在下面这个global queue中的:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // ....... }
- 接着就是执行这个transform函数了:
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
- 如果获得了新的transformedImage,不管transform后是否改变了图片.都要存储到缓存中。区别在于如果transform后的图片和之前不一样,就需要重新生成imageData,而不能在使用之前最初的那个imageData了。
- 最后,如果operation未被取消,就调用completedBlock:
dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url); } });
- 最后是else
// 和上面else if一样,根据一个key将downloadedImage存储到缓存,不过此处不需要重新计算data的 if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } // operation没被取消,就调用completedBlock dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } });
4. 总结
到目前为止,我们整个代码其实就是为了创建一个NSOperation,然后利用NSURLConnection去下载图片。下面一篇会具体说说NSURLConnection如何下载图片的。