【原】SDWebImage源码阅读(三)

时间:2023-02-21 13:33:37

【原】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。这下我们就明白了其中的关系,如图:

【原】SDWebImage源码阅读(三)

这个函数还有一处要注意,就是如果当前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. 1.调用自定义的cancelBlock。
  2. 2.调用NSURLConnection的cancel取消self.connection。
  3. 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, ), ^{
// .......
}
        接着就是执行这个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如何下载图片的。

5. 参考文章


【原】SDWebImage源码阅读(三)的更多相关文章

  1. 【原】SDWebImage源码阅读(五)

    [原]SDWebImage源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 前面的代码并没有特意去讲SDWebImage的缓存机制,主要是想单独开一章节专门讲 ...

  2. 【原】SDWebImage源码阅读(四)

    [原]SDWebImage源码阅读(四) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 SDWebImage中主要实现了NSURLConnectionDataDelega ...

  3. 【原】SDWebImage源码阅读(二)

    [原]SDWebImage源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 解决上一篇遗留的坑 上一篇中对sd_setImageWithURL函数简单分析了一下,还 ...

  4. 【原】SDWebImage源码阅读(一)

    [原]SDWebImage源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 一直没有系统地读过整套源码,就感觉像一直看零碎的知识点,没有系统读过一本专业经典书 ...

  5. SDWebImage源码阅读-第三篇

    这一篇讲讲不常用的一些方法. 1 sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress: com ...

  6. 25 BasicUsageEnvironment0基本使用环境基类——Live555源码阅读(三)UsageEnvironment

    25 BasicUsageEnvironment0基本使用环境基类——Live555源码阅读(三)UsageEnvironment 25 BasicUsageEnvironment0基本使用环境基类— ...

  7. 26 BasicUsageEnvironment基本使用环境——Live555源码阅读(三)UsageEnvironment

    26 BasicUsageEnvironment基本使用环境--Live555源码阅读(三)UsageEnvironment 26 BasicUsageEnvironment基本使用环境--Live5 ...

  8. 24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment

    24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment 24 UsageEnvironment使用环境抽象基类——Live555源码阅读 ...

  9. SDWebImage 源码阅读分享

    SDWebImage 源码阅读分享 疑问列表 SDWebImage 整体框架图,主要的类包含哪些 SDWebImage 如何进行缓存管理,过期失效策略,缓存更新 SDWebImage 如何多线程处理的 ...

随机推荐

  1. 非阻塞socket学习,select基本用法

    server #include <stdio.h> #include <winsock2.h> #include <iostream> #pragma commen ...

  2. Mac 如果一定要写个锁屏程序的话就这样

    package test; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import ...

  3. UIKit类结构图

  4. 设计模式之 - 代理模式(Proxy Pattern)

    代理模式:代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理.很多可以框架中都有用 ...

  5. 我是如何通过学习拿到年薪80w

    本人做了5年Java,如今还是一个码农,天天写业务代码,直到2018年10月中旬遇到一位技术大牛,他给我一套技术思维导图让我又看到了希望!今天分享给各位想要提升.升职.加薪的你. 普通Java程序员与 ...

  6. 基础篇:8&period;如何定义变量?js变量有什么特点?

    书接上文,废话不多说,直接进入正题,下面我们一起来讨论js中的变量那些事! 那什么是变量? 变量是存储信息的容器,可以存储任何类型的数据. 如何定义变量呢? 变量可以使用短名称,如x,y:也可以是长名 ...

  7. BZOJ&period;1299&period;&lbrack;LLH邀请赛&rsqb;巧克力棒&lpar;博弈论 Nim&rpar;

    题目链接 \(Description\) 两人轮流走,每次可以从盒子(容量给定)中取出任意堆石子加入Nim游戏,或是拿走任意一堆中正整数个石子.无法操作的人输.10组数据. \(Solution\) ...

  8. bzoj2242&colon; &lbrack;SDOI2011&rsqb;计算器 BSGS&plus;exgcd

    你被要求设计一个计算器完成以下三项任务: 1.给定y,z,p,计算Y^Z Mod P 的值:(快速幂) 2.给定y,z,p,计算满足xy≡ Z ( mod P )的最小非负整数:(exgcd) 3.给 ...

  9. FragmentStatePagerAdapter写法

    为了节省资源,分批加载数据//适配器class TabLayoutViewPagerAdapter extends FragmentStatePagerAdapter { public TabLayo ...

  10. uva11361 特殊数的数量(数位dp)

    题目传送门 题目大意:给你一个n-m的区间,问你这个闭区间内的特殊数有几个,特殊数的要求是 数的本身 和 各位数字之和  mod k 等于0. 思路:刚接触数位dp,看了网上的题解,说用dp[i][j ...