1.SDWebImageDownloader中的downloadImageWithURL
我們來到SDWebImageDownloader.m文件中,找到downloadImageWithURL函數(shù)。發(fā)現(xiàn)代碼不是很長(zhǎng),那就一行行讀。畢竟這個(gè)函數(shù)大概做什么我們是知道的。這個(gè)函數(shù)大概就是創(chuàng)建了一個(gè)SDWebImageSownloader的異步下載器,根據(jù)給定的URL下載image。
先映入眼簾的是下面兩行代碼,簡(jiǎn)單地開開胃:
// 封裝了異步下載圖片操作
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
接著又是一個(gè)函數(shù)直接到底:addProgressCallback。這是SDWebImageDownloader的私有函數(shù),所以直接一點(diǎn)點(diǎn)看它實(shí)現(xiàn)。
// 這里的url不能為空,下面會(huì)解釋。如果為空,completedBlock中image、data和error直接傳入nil
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
之所以u(píng)rl不能為空,是因?yàn)檫@個(gè)url要作為NSDictionary變量的key值,所以不能為空。而這個(gè)NSDictionary變量就是URLCallbacks。我們從名稱大概可以猜到,這個(gè)NSDictionary應(yīng)該是存儲(chǔ)每個(gè)url對(duì)應(yīng)的callback(本質(zhì)是因?yàn)橐粋€(gè)url基本上對(duì)應(yīng)一個(gè)網(wǎng)絡(luò)請(qǐng)求,而每個(gè)網(wǎng)絡(luò)請(qǐng)求就是一個(gè)SDWebImageDownloaderOperation,而這個(gè)SDWebImageDownloaderOperation初始化是使用initWithRequest進(jìn)行的,initWithRequest需要提供這些callbacks)。那對(duì)應(yīng)的callback函數(shù)都有哪些呢?
我們先找到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是一個(gè)NSMutableArray類型,那它其中對(duì)應(yīng)的每個(gè)object存儲(chǔ)的是什么呢?看addObject:callbacks,原來是callbacks。那callbacks又是什么?居然是一個(gè)NSMutableDictionary類型。而且存儲(chǔ)了對(duì)應(yīng)的progressBlock和completedBlock。這下我們就明白了其中的關(guān)系,如圖:

這個(gè)函數(shù)還有一處要注意,就是如果當(dāng)前url是第一次請(qǐng)求,也就是說對(duì)應(yīng)的URLCallbacks[url]為空,那就新建一個(gè),同時(shí)置first為YES,就是說這是第一次創(chuàng)建該url的callbacks。而且還會(huì)調(diào)用createCallback,相當(dāng)于第一次初始化過程。
另外整個(gè)代碼是放在下面的dispatch_barrier_sync中:
dispatch_barrier_sync(self.barrierQueue, ^{
//...
});
因?yàn)榇撕瘮?shù)可能會(huì)有多個(gè)線程同時(shí)執(zhí)行(因?yàn)樵试S多個(gè)圖片的同時(shí)下載),那么就有可能會(huì)有多個(gè)線程同時(shí)修改URLCallbacks,所以使用dispatch_barrier_sync來保證同一時(shí)間只有一個(gè)線程在訪問URLCallbacks。并且此處使用了一個(gè)單獨(dú)的queue--barrierQueue,并且這個(gè)queue是一個(gè)DISPATCH_QUEUE_CONCURRENT類型的。也就是說,這里雖然允許你針對(duì)URLCallbacks的操作是并發(fā)執(zhí)行的,但是因?yàn)槭褂昧薲ispatch_barrier_sync,所以你必須保證之前針對(duì)URLCallbacks的操作要完成才能執(zhí)行下面針對(duì)URLCallbacks的操作。
注意:我發(fā)現(xiàn)使用barrierQueue的都是dispatch_barrier_sync、dispatch_barrier_async、dispatch_sync,我就納悶了,這些有用到并發(fā)的東西嗎?為什么不直接使用DISPATCH_QUEUE_SERIAL。
總的來說,上面那個(gè)addProgressCallback函數(shù)主要就是生成了每個(gè)url的callbacks,并且以URLCallbacks形式傳遞給別人。具體我們回到downloadImageWithURL中再看。
回到downloadImageWithURL函數(shù)中的addProgressCallback中,看到它具體的createCallback實(shí)現(xiàn)。代碼不是很長(zhǎng)。也是按順序看:
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
downloadTimeOut表示的下載超時(shí)的限定時(shí)間,默認(rèn)是15秒。
然后再往下看就傻眼了,之前對(duì)iOS的網(wǎng)絡(luò)部分一竅不通啊。沒辦法,硬著頭皮,一點(diǎn)點(diǎn)死扣吧。
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
首先要知道initWithURL函數(shù)是做什么的?看看注釋,大概明白了。就是根據(jù)url,緩存策略(cachePolicy)和超時(shí)限定時(shí)間(timeoutInterval)來產(chǎn)生一個(gè)NSURLRequest。這里比較麻煩的是cachePolicy,就是告訴這個(gè)request(請(qǐng)求)如何緩存結(jié)果:
(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData)
- SDWebImageDownloaderUseNSURLCache:在SDWebImage中,缺省情況下,request是不使用NSURLCache的,但是若使用該選項(xiàng),就默認(rèn)使用NSURLCache默認(rèn)的緩存策略:NSURLRequestUseProtocolCachePolicy。
- NSURLRequestUseProtocolCachePolicy:對(duì)特定的 URL 請(qǐng)求使用網(wǎng)絡(luò)協(xié)議(如HTTP)中實(shí)現(xiàn)的緩存邏輯。這是默認(rèn)的策略。該策略表示如果緩存不存在,直接從服務(wù)端獲取。如果緩存存在,會(huì)根據(jù)response中的Cache-Control字段判斷 下一步操作,如: Cache-Control字段為must-revalidata, 則 詢問服務(wù)端該數(shù)據(jù)是否有更新,無更新話 直接返回給用戶緩存數(shù)據(jù),若已更新,則請(qǐng)求服務(wù)端.
-
NSURLRequestReloadIgnoringLocalCacheData:數(shù)據(jù)需要從原始地址(一般就是重新從服務(wù)器獲取)加載。不使用現(xiàn)有緩存。
接下來就是設(shè)置request的一些屬性了(可以看出此處使用的實(shí)HTTP協(xié)議):
// 如果設(shè)置HTTPShouldHandleCookies為YES,就處理存儲(chǔ)在NSHTTPCookieStore中的cookies。
// HTTPShouldHandleCookies表示是否應(yīng)該給request設(shè)置cookie并隨request一起發(fā)送出去。
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
// HTTPShouldUsePipelining表示receiver(理解為iOS客戶端)的下一個(gè)信息是否必須等到上一個(gè)請(qǐng)求回復(fù)才能發(fā)送。
// 如果為YES表示可以,NO表示必須等receiver收到先前的回復(fù)才能發(fā)送下個(gè)信息。
request.HTTPShouldUsePipelining = YES;
// 如果你設(shè)置了SDWebImageDownloader的headersFilter,就是用你自定義的方法,來設(shè)置HTTP的header field。
// 如果沒有自定義,就是用SDWebImage提供的HTTPHeaders。
// 簡(jiǎn)單看下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來初始化一個(gè)operation。細(xì)節(jié)暫且不看,直接跳過,后面的看完再來好好研究。先看下面:
operation.shouldDecompressImages = wself.shouldDecompressImages;
這個(gè)簡(jiǎn)單,就是說要不要解壓縮圖片。解壓縮已經(jīng)下載的圖片或者在緩存中的圖片,可以提高性能,但是會(huì)耗費(fèi)很多空間,缺省情況下是要解壓縮圖片。
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是一個(gè)NSURLCredential類型。
知識(shí)點(diǎn):NSURLCredential
1.web 服務(wù)可以在返回 http 響應(yīng)時(shí)附帶認(rèn)證要求的challenge,作用是詢問 http 請(qǐng)求的發(fā)起方是誰,這時(shí)發(fā)起方應(yīng)提供正確的用戶名和密碼(即認(rèn)證信息),然后 web 服務(wù)才會(huì)返回真正的 http 響應(yīng)。
2.收到認(rèn)證要求時(shí),NSURLConnection 的委托對(duì)象會(huì)收到相應(yīng)的消息并得到一個(gè) NSURLAuthenticationChallenge 實(shí)例。該實(shí)例的發(fā)送方遵守 NSURLAuthenticationChallengeSender 協(xié)議。為了繼續(xù)收到真實(shí)的數(shù)據(jù),需要向該發(fā)送方向發(fā)回一個(gè) NSURLCredential 實(shí)例。
如果已經(jīng)有了credential,那就直接賦值。如果沒有,就用用戶名(username)和密碼(password)新構(gòu)建一個(gè):
[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
其中NSURLCredentialPersistenceForSession表示在應(yīng)用終止時(shí),丟棄相應(yīng)的 credential 。
接著是設(shè)置該operation的優(yōu)先級(jí),畢竟operation對(duì)應(yīng)一個(gè)NSOperation。
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
這個(gè)簡(jiǎn)單,就是優(yōu)先級(jí)設(shè)定,一般來說,優(yōu)先級(jí)越高,執(zhí)行越早。
然后就是添加到NSOperationQueue中,這個(gè)downloadQueue一看就知道肯定是NSOperationQueue,代碼如下:
[wself.downloadQueue addOperation:operation];
最后是處理operation的執(zhí)行順序:
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 如果執(zhí)行順序?yàn)長(zhǎng)IFO(last in first out,后進(jìn)先出,棧結(jié)構(gòu))
// 就將新添加的operation作為最后一個(gè)operation的依賴,就是說,要執(zhí)行最后一個(gè)operation,必須先執(zhí)行完新添加的operation,這就實(shí)現(xiàn)了棧結(jié)構(gòu)。
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
剛才說的都是對(duì)operation的一些屬性設(shè)置?,F(xiàn)在可以回到operation創(chuàng)建的那個(gè)函數(shù)initWithRequest中了。順便提一句,initWithRequest是SDWebImageDownloaderOperation函數(shù),所以前面[wself.operationClass]返回的是SDWebImageDownloaderOperation(不相信的話,請(qǐng)搜索setOperationClass)。這也是一個(gè)編程技巧,把Class類型作為屬性存起來。
// 先看看這個(gè)函數(shù)聲明和注釋,返回的是SDWebImageDownloaderOperation。
// 參數(shù)需要request,不過這個(gè)上面的代碼已經(jīng)創(chuàng)建好了,而options使用的是downloadImageWithURL傳入的options
// 真正需要在傳遞給此函數(shù)的就剩下三個(gè)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);
});
}
}
其中主要難點(diǎn)在下面這段代碼:
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
注意此處使用了同步方法dispatch_sync,也就是說,callbacksForURL這條賦值語句是放在barrierQueue線程執(zhí)行的,而且此時(shí)會(huì)阻塞當(dāng)前線程。我們之前提到過,barrierQueue是為了保證同一時(shí)刻只有一個(gè)線程對(duì)URLCallbacks進(jìn)行操作。說實(shí)話,我不是很明白這里為什么要使用dispatch_sync,為什么不用dispatch_barrier_sync?希望大神可以告知原因。(此處我回頭想了下,可能是因?yàn)閷?duì)于同一個(gè)圖片下載任務(wù),會(huì)不停地調(diào)用progressBlock函數(shù),這個(gè)callbacksForURL的賦值語句可能是在同一個(gè)圖片下載任務(wù)的不同的線程(一個(gè)圖片每次下載到新數(shù)據(jù)后調(diào)用progressblock)中執(zhí)行的,但是你必須要保證前一部分?jǐn)?shù)據(jù)下載任務(wù)完成,才能執(zhí)行后一部分?jǐn)?shù)據(jù)的下載任務(wù),此處需要同步,所以使用dispatch_sync,此處單獨(dú)使用一個(gè)barrierQueue,還可以防止dispatch_sync造成死鎖)。
跟著的for循環(huán)就好理解了,直接從callbacks中索引到progressBlock,放入主線程中進(jìn)行下載,當(dāng)然,下載過程中肯定要知道已經(jīng)下載了多少(receivedSize)和預(yù)期下載的大小(expectedSize)。因?yàn)檫@個(gè)block是不停調(diào)用,只要有新的數(shù)據(jù)到達(dá)就調(diào)用,直到下載完成,所以這兩個(gè)參數(shù)還是必備的,判斷是否下載完成。
下面的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。不同圖片的下載任務(wù)會(huì)異步完成,所以必要保證之前其他圖片下載完成,并執(zhí)行完completedBlock內(nèi)的對(duì)URLCallbacks的操作,才能接著運(yùn)行。因?yàn)橹灰戎暗倪M(jìn)程完成,并不需要關(guān)心之前的進(jìn)程是不是同步執(zhí)行,所以使用的是dispatch_barrier_sync。其他邏輯部分,很簡(jiǎn)單,就不贅述了。
最后是cancelBlock:
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}
因?yàn)槿∠耍灾苯影製rl從URLCallbacks中移除。但是此處同步方案又是用dispatch_barrier_async。其實(shí)我覺得在同一個(gè)queue中,使用dispatch_barrier_async還是使用dispatch_barrier_sync并沒有什么區(qū)別。因?yàn)槎际且戎暗膱?zhí)行完成。(不過dispatch_barrier_async表示的是先等之前的執(zhí)行完成,然后把該barrier放入queue中,而不是等待barrier中代碼執(zhí)行結(jié)束,而dispat_barrier_sync表示需要等待barrier中代碼執(zhí)行結(jié)束)。
2. 運(yùn)行
之前這個(gè)系列的博客都是為了構(gòu)造一個(gè)operation(NSOperation),并且也放到downloadQueue(NSOperationQueue)。但是我們還需要點(diǎn)火啟動(dòng)這個(gè)operation。
我們實(shí)現(xiàn)了NSOperation的子類,那么要讓其運(yùn)行起來,要么實(shí)現(xiàn)main(),要么實(shí)現(xiàn)start()。這里SDWebImageDownloaderOperation選擇實(shí)現(xiàn)了start()。我們先一步步看看start()實(shí)現(xiàn):
先是一個(gè)線程線程同步鎖(以self作為互斥信號(hào)量):
@synchronized (self) {
// ...
}
此處到底寫了什么代碼,居然需要同步,而且還是以加鎖的方式?
首先是判斷當(dāng)前這個(gè)SDWebImageDownloaderOperation是否取消了,如果取消了,即認(rèn)為該任務(wù)已經(jīng)完成,并且及時(shí)回收資源(即reset)。
這里簡(jiǎn)單介紹下NSOperation的三個(gè)重要的狀態(tài),如果你使用了NSOperation,就需要手動(dòng)管理這三個(gè)重要的狀態(tài):
- isExecuting 代表任務(wù)正在執(zhí)行中
- isFinished 代表任務(wù)已經(jīng)執(zhí)行完成
- isCancelled 代表任務(wù)已經(jīng)取消執(zhí)行
if (self.isCancelled) {
self.finished = YES;
[self reset]; // 資源回收,資源全部置為nil,自動(dòng)回收
return;
}
然后是一段宏中的代碼,這段代碼主要是考慮到app進(jìn)入后臺(tái)發(fā)生的事,雖然代碼很簡(jiǎn)單,但是有些技巧還是需要學(xué)習(xí)的:
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;
}
}];
}
因?yàn)橐褂胋eginBackgroundTaskWithExpirationHandler,所以需要使用[UIApplication sharedApplication],因?yàn)槭堑谌綆?kù),所以需要使用NSClassFromString獲取到UIApplication。這里需要提及的就是shouldContinueWhenAppEntersBackground,也就是說下載選項(xiàng)中需要設(shè)置SDWebImageDownloaderContinueInBackground。
注意beginBackgroundTaskWithExpirationHandler并不是意味著立即執(zhí)行后臺(tái)任務(wù),它只是相當(dāng)于注冊(cè)了一個(gè)后臺(tái)任務(wù),函數(shù)后面的handler block表示程序在后臺(tái)運(yùn)行時(shí)間到了后,要運(yùn)行的代碼**。這里,后臺(tái)時(shí)間結(jié)束時(shí),如果下載任務(wù)還在進(jìn)行,就取消該任務(wù),并且調(diào)用endBackgroundTask,以及置backgroundTaskId為UIBackgroundTaskInvalid。
注意此處取消任務(wù)的方法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會(huì)不存在呢?
具體看cancelInternalAndStop和cancelInternal代碼,發(fā)現(xiàn)cancelInternalAndStop就多了一行代碼:
CFRunLoopStop(CFRunLoopGetCurrent());
因?yàn)槊總€(gè)NSThread都會(huì)有一個(gè)CFRunLoop(后面的代碼會(huì)有CFRunLoopRun函數(shù)出現(xiàn)),所以如果要取消的話,就得同時(shí)stop這個(gè)RunLoop。所以cancel函數(shù)的邏輯主要就是cancelIntenal函數(shù)了。
cancelIntenal函數(shù)所做了三件事:
1.調(diào)用自定義的cancelBlock。
2.調(diào)用NSURLConnection的cancel取消self.connection。
3.回收資源。
注意到在取消self.connection過程中,發(fā)送了一個(gè)SDWebImageDownloadStopNotification的通知。我們可以看到這個(gè)通知注冊(cè)的地方是在SDWebImageDownloader類的initialize函數(shù):
+ (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];
}
}
注意到如果你要使用這個(gè)SDWebImageDownloadStopNotification通知,需要綁定SDNetworkActivityIndicator,這個(gè)貌似是需要單獨(dú)下載的。當(dāng)然,你可以修改這部分源代碼,換成別的ActivityIndicator。
這里就有疑問了,此時(shí)我們的backgroundTaskId已經(jīng)注冊(cè)過了,如果此NSOperation在進(jìn)入后臺(tái)運(yùn)行之前就已經(jīng)完成任務(wù)了,不就應(yīng)該把這個(gè)backgroundTaskId置為UIBackgroundTaskInvalid嗎,意思就是告訴系統(tǒng),任務(wù)完成,不需要考慮進(jìn)不進(jìn)入后臺(tái)運(yùn)行的問題了。確實(shí),在start函數(shù)末尾,就是判斷如果下載任務(wù)完成(不管有沒有下載成功),就將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];
注冊(cè)過后臺(tái)代碼后,接著就是要正式運(yùn)行了。所以先要置executing屬性為YES。然后就是關(guān)鍵的connection了。connection是一個(gè)NSURLConnection類型的屬性。這里我們能感覺到,真正的下載圖片的網(wǎng)絡(luò)處理部分就是利用了NSURLConnection。此處使用的self.request就是上面提到的那個(gè)NSMutableURLRequest(在SDWebImageDownloader.m中的downloadImageWithURL函數(shù)中生成的)。其實(shí)我們現(xiàn)在應(yīng)該看下SDWebImageDownloaderOperation中實(shí)現(xiàn)的NSURLConnectionDataDelegate方法。但是不急,先把start函數(shù)中的剩下函數(shù)看完。剩下的不是很難,所以先解決。
雖然已經(jīng)使用init方法構(gòu)建了一個(gè)NSURLConnection,但是真正要啟動(dòng)下載還需要使用NSURLConnection的start方法。
[self.connection start];
接下來就是判斷這個(gè)connection是否創(chuàng)建成功:
if (self.connection) {
// ......
} else {
// ......
}
這個(gè)if else語句要分一下兩個(gè)情形討論:
情形1:connection創(chuàng)建成功
1.因?yàn)閯俢onnection剛start,所以此處執(zhí)行的progresBlock的參數(shù)為receivedSize=0,expectedSize=NSURLResponseUnknownLength(((long long)-1))。我們都知道一般除非自定義progressBlock,不然一般progresBlock為nil。所以如果這里用戶自定義了progressBlock,但是這是用戶定義的行為,為什么要將參數(shù)設(shè)置成這樣呢?我不是很清楚,但是用戶在設(shè)計(jì)自己的progressBlock的時(shí)候就要留心這個(gè)參數(shù)問題了,要特意處理expectedSize為NSURLResponseUnknownLength的情況。
2.接著回到主進(jìn)程使用SDWebImageDownloadStartNotification,和之前說的SDWebImageDownloadStopNotification有異曲同工之處。讀者可以自己查詢。
3.接下來就是調(diào)用RunLoop了。這里它以NSFoundation的iOS5.1版本作為分界線進(jìn)行討論的,不過兩者做的事情都一樣,只不過調(diào)用函數(shù)不同罷了——都是調(diào)用RunLoop直到下載任務(wù)終止或者完成。
4.這是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);
}
5.稍微提一下CFRunLoopRun,大概能看出來這是一個(gè)while循環(huán),并且是在使用CFRunLoopGetCurrent()來不停地執(zhí)行當(dāng)前RunLoop的任務(wù),直到任務(wù)被終止或者完成。
6.你可以這樣理解這兩個(gè)函數(shù)關(guān)系,CFRunLoopRun就是使用默認(rèn)mode運(yùn)行的CFRunLoopRunInMode。至于為什么iOS5.1之前的要使用CFRunLoopRunInMode,我們從其中的注釋也可以看出,其實(shí)主要是利用CFRunLoopRunInMode的CFTimeInterval seconds參數(shù)。
7.那么執(zhí)行當(dāng)前進(jìn)程的任務(wù)到底指什么?具體請(qǐng)看這篇文章--深入理解RunLoop。簡(jiǎn)單點(diǎn)說,這里進(jìn)程主要是響應(yīng)NSURLConnectionDataDelegate和NSURLConnectionDelegate的各種代理函數(shù)。
8.通常使用 NSURLConnection 時(shí),你會(huì)傳入一個(gè) delegate,當(dāng)調(diào)用了 [self.connection start] 后,這個(gè)delegate 就會(huì)不停收到事件回調(diào)。所以也就是說等這個(gè)connection完成或者終止,才會(huì)跳出CFRunLoopRun()。當(dāng)跳出Runloop后,就要判斷NSURLConnection是不是正常完成任務(wù)了。如果沒有,也就是說self.isFinished == NO。那么就取消該connection,并且調(diào)用- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;返回錯(cuò)誤信息,打印出錯(cuò)的請(qǐng)求url。總的代碼如下:
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
情形2:connection創(chuàng)建失敗
調(diào)用completedBlock。因?yàn)榇颂幨鞘×?,所以image和data參數(shù)為nil,而error從它的NSLocalizedDescriptionKey就可以看出Connection can't be initialized。
3. SDWebImageManager中的downloadImageWithURL剩余部分
其實(shí)我們只剩下了SDWebImageDownloader的downloadImageWithURL中的completedBlock部分還沒細(xì)說了。
completedBlock也分為三種情形:
3.1 情形1:operation(非subOperation)取消了
什么都不做。因?yàn)槿绻阋诖颂幷{(diào)用completedBlock的話,可能會(huì)存在和其他的completedBlock產(chǎn)生條件競(jìng)爭(zhēng),可能會(huì)修改同一個(gè)數(shù)據(jù)。
if (weakOperation.isCancelled) {
// ......
}
3.2 情形2:download產(chǎn)生了錯(cuò)誤error
else if (error) {
// ......
}
首先先判斷operation是否取消了(檢查是否取消要勤快點(diǎn)),沒有取消,就調(diào)用completedBlock,處理error。
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
隨后檢查錯(cuò)誤類型,確認(rèn)不是客戶端或者服務(wù)器端的網(wǎng)絡(luò)問題,就認(rèn)為這個(gè)url本身問題了。并把這個(gè)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
如果使用了SDWebImageRetryFailed選項(xiàng),那么即使該url是failedURLs,也要從failedURLs移除,并繼續(xù)執(zhí)行download:
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
cacheOnDisk表示是否使用磁盤上的緩存:
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
接著又是一個(gè)if else。我們先大概看看框架:
// image是從SDImageCache中獲取的,downloadImage是從網(wǎng)絡(luò)端獲取的
// 所以雖然options包含SDWebImageRefreshCached,需要刷新imageCached,
// 并使用downloadImage,不過可惜downloadImage沒有從網(wǎng)絡(luò)端獲取到圖片。
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// ......
}
// 圖片下載成功,獲取到了downloadedImage。
// 這時(shí)候如果想transform已經(jīng)下載的圖片,就得先判斷這個(gè)圖片是不是animated image(動(dòng)圖),
// 這里可以通過downloadedImage.images是不是為空判斷。
// 默認(rèn)情況下,動(dòng)圖是不允許transform的,不過如果options選項(xiàng)中有SDWebImageTransformAnimatedImage,也是允許transform的。
// 當(dāng)然,靜態(tài)圖片不受此干擾。另外,要transform圖片,還需要實(shí)現(xiàn)
// transformDownloadedImage這個(gè)方法,這個(gè)方法是在SDWebImageManagerDelegate代理定義的
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
// ......
else { // 這個(gè)不用解釋了
}
接著我們就可以具體看看每個(gè)判斷里面的實(shí)現(xiàn)了:
- 首先是if,滿足這種情況,就不需要調(diào)用completedBlock。
- 然后是else if,滿足這種情況,首先肯定要將downloadedImage進(jìn)行transform。
不過我們先看下transformDownloadedImage的注釋:
// 允許在image剛下載完,以及在緩存到內(nèi)存和disk之前,進(jìn)行transform。
// 注意:該方法是在一個(gè)global queue中調(diào)用,為了避免阻塞主線程。
所以我們可以看到整個(gè)else if中的語句是包含在下面這個(gè)global queue中的:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// .......
}
接著就是執(zhí)行這個(gè)transform函數(shù)了:
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
如果獲得了新的transformedImage,不管transform后是否改變了圖片.都要存儲(chǔ)到緩存中。區(qū)別在于如果transform后的圖片和之前不一樣,就需要重新生成imageData,而不能在使用之前最初的那個(gè)imageData了。
最后,如果operation未被取消,就調(diào)用completedBlock:
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
- 最后是else
// 和上面else if一樣,根據(jù)一個(gè)key將downloadedImage存儲(chǔ)到緩存,不過此處不需要重新計(jì)算data的
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
// operation沒被取消,就調(diào)用completedBlock
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
4. 總結(jié)
到目前為止,我們整個(gè)代碼其實(shí)就是為了創(chuàng)建一個(gè)NSOperation,然后利用NSURLConnection去下載圖片。下面一篇會(huì)具體說說NSURLConnection如何下載圖片的。
5. 參考文章
- NSURLCache
- NSURLCredential 身份認(rèn)證
- GCD有關(guān)問題:dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@"Hello ?");}); 死鎖的原因
- Cocoa深入學(xué)習(xí):NSOperationQueue、NSRunLoop和線程安全
- 深入理解RunLoop
本文轉(zhuǎn)載polobymulberry-博客園