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

QQ20151204-0@2x

这个函数还有一处要注意,就是如果当前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, 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如何下载图片的。

5. 参考文章


posted @ 2015-12-26 19:51  桑果  阅读(2706)  评论(13编辑  收藏  举报