【原】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如何下載圖片的。