【原】SDWebImage源碼閱讀(三)

【原】SDWebImage源碼閱讀(三)

本文轉載請註明出處 —— polobymulberry-博客園html

1.SDWebImageDownloader中的downloadImageWithURL


咱們來到SDWebImageDownloader.m文件中,找到downloadImageWithURL函數。發現代碼不是很長,那就一行行讀。畢竟這個函數大概作什麼咱們是知道的。這個函數大概就是建立了一個SDWebImageSownloader的異步下載器,根據給定的URL下載image。ios

先映入眼簾的是下面兩行代碼,簡單地開開胃:git

// 封裝了異步下載圖片操做
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;

接着又是一個函數直接到底:addProgressCallback。這是SDWebImageDownloader的私有函數,因此直接一點點看它實現。github

// 這裏的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函數都有哪些呢?web

咱們先找到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中:cookie

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. 參考文章


相關文章
相關標籤/搜索