• 【原】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. 参考文章


  • 相关阅读:
    [ZT]数据表:USR02(登录数据)
    ASP.NET Web 应用程序与ASP.NET网站比较
    DataTable排序的一般方法
    如何重建開啟被锁的SAP超级用戶帐号
    SQL語句中时间格式的转换
    [ZT]提高 Ajax 应用程序性能,避开 Web 服务漏洞
    SAP NetWeaver
    SAP安装步骤[ECC6]WIN2000AS
    SAP&SAP Solution Manager中的常用命令
    [转]如何在Visio 2007中画接口和实现类的关系图
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/5017995.html
Copyright © 2020-2023  润新知