SDWebImageManager
這座橋樑,有效控制了圖片下載的時機和同快取的協同操作。這篇來關注一下在 SD 中,Downloader Class 的具體實現。
Downloader 中的一些列舉
在 SDWebImageDownloader.m
中,可以發現這麼一個屬性:
1 |
@property (strong, nonatomic) NSOperationQueue *downloadQueue; |
NSOperation
表示一個獨立的控制單元,也就是我們所說的執行緒。而 NSOperationQueue
控制著這些並行操作的執行,以佇列的資料結構特點,從而實現執行緒優先順序的控制。而在 SDWebImage
中,很顯然是用來管理 SDWebImageDownloaderOperation
。對於 SDWebImageDownloaderOperation
後面將會單獨放在一篇博文中介紹。
同 Manager 一樣,我們先來看看在 .h
檔案中所有的下載模式列舉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { // 低優先順序(常用) SDWebImageDownloaderLowPriority = 1 0, // 顯示下載程式 SDWebImageDownloaderProgressiveDownload = 1 1, // 預設情況下是不需要 NSURLCache 的。 // 如果啟用這個模式,快取策略將會更改成 NSURLCache SDWebImageDownloaderUseNSURLCache = 1 2, // 如果圖片是從 NSURLCache 中讀取到的,則使用 nil 來作為回撥 block 的傳入引數 // 常常會與 SDWebImageDownloaderUseNSURLCache 組合使用 SDWebImageDownloaderIgnoreCachedResponse = 1 3, // 當裝置為 iOS 4 以上的情況,則在後臺可以繼續下載圖片 // 通過向系統額外申請時間來完成資料請求操作 // 如果後臺任務終止,則操作會取消 SDWebImageDownloaderContinueInBackground = 1 4, // 設定 NSMutableURLRequest.HTTPShouldHandleCookies = YES // 從而處理儲存在 NSHTTPCookieStore 的 cookie SDWebImageDownloaderHandleCookies = 1 5, // 允許使用不受信的 SSL 證書 // 主要用於測試 // 常用在開發環境下 SDWebImageDownloaderAllowInvalidSSLCertificates = 1 6, // 圖片放在優先順序更高的佇列中 SDWebImageDownloaderHighPriority = 1 7, }; |
另外,對於下載順序,SD 也為我們提供了兩種不同的下載順序列舉:
1 2 3 4 5 6 |
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { // 先進先出 預設操作順序 SDWebImageDownloaderFIFOExecutionOrder, // 後進先出 SDWebImageDownloaderLIFOExecutionOrder }; |
options 列舉已經幾乎將所有的開發場景所需要的模式考慮進來。下面我們來看一看具體的實現程式碼。
Downloader 的私有成員物件
先來看下 Class 的 property 物件的作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@interface SDWebImageDownloader () // NSOperation 操作佇列 @property (strong, nonatomic) NSOperationQueue *downloadQueue; // 最後新增的 Operation ,順序為後進先出順序 @property (weak, nonatomic) NSOperation *lastAddedOperation; // 圖片下載類 @property (assign, nonatomic) Class operationClass; // URL 回撥字典 // key 是圖片的 URL // value 是一個陣列,包含每個圖片的回撥資訊 @property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // HTTP 頭資訊 @property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; // 並行的處理所有下載操作的網路響應 // 實現網路序列化的例項 // 對於 URLCallbacks 的所有修改,都需要放在 barrierQueue 中,並通過 dispatch_barrier_sync 形式 // 用於保證執行緒安全性 @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; @end |
由於需要保證多個圖片可以同時下載,為了保證 URLCallbacks
的執行緒安全,我們使用 GCD 中的 dispatch_barrier_sync
為程式設定柵欄(barrier),它會等待所有位於柵欄函式之前的操作執行完成後再執行,並且在柵欄函式執行完成後,其後續操作才會開始執行,這個函式需要同 dispatch_queue_create
生成的 Dispatch 的同步佇列(Concurrent Dispatch Queue)共同使用。
有了這些對於類成員的認識,開始閱讀 Downloader 的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
/** * 下載操作 * * @param url 下載 URL * @param options 下載操作選項 * @param progressBlock 過程 block * @param completedBlock 完成 block * * @return 遵循 SDWebImageOperation 協議的物件 */ - (id SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { // 定義下載 operation __block SDWebImageDownloaderOperation *operation; // weakly self 接觸引用環 __weak __typeof(self)wself = self; // 新增回撥閉包,傳入URL、過程 block、完成 block [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ // 設定下載時限,預設為 15 秒 NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } // 建立 HTTP 請求,並根據下載模式列舉設定相關屬性 // 為了防止有可能出現的重複快取問題,如果沒有顯式宣告需要快取管理,則不啟用圖片請求的快取操作 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; // 是否處理 cookie request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); // 是否需要傳輸資料 // 返回在接到上一個請求的響應之前,是否需要傳輸資料 request.HTTPShouldUsePipelining = YES; // 設定請求頭,需要根據需要過濾指定 URL if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; } // 建立下載 operation operation = [[wself.operationClass alloc] initWithRequest:request inSession:self.session options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { // strongly self,保證生命週期 SDWebImageDownloader *sself = wself; if (!sself) return; // URL 回撥陣列,以 URL 為 key 儲存回撥 callback __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ // 從全域性字典中獲取指定的 callback callbacksForURL = [sself.URLCallbacks[url] copy]; }); for (NSDictionary *callbacks in callbacksForURL) { // 執行執行時指定圖片的回撥 block dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { // strongly self, 保證生命週期 SDWebImageDownloader *sself = wself; if (!sself) return; // 完成時 callback 取方法與上方相同 // 因為是使用字典進行管理 __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); } } cancelled:^{ // strongly self,保證生命週期 SDWebImageDownloader *sself = wself; if (!sself) return; // 與前方的執行操作進行柵欄隔離操作 // 保證在刪除的時候沒有執行自定對於 callback 的讀寫操作 dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; // 是否需要對圖片進行壓縮處理 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]; } // 設定下載操作的優先順序操作,需要根據下載模式列舉來判斷 if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } // 向下載操作的佇列中增加當前操作 [wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // 如果執行順序為後進先出的棧結構 // 則將新新增的 operation 作為當前最後一個 operation 的依賴,按照順序逐個執行 [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; |
整個流程已經瞭解,下面分析一些細小的細節問題:
全域性字典,將 URL 與回撥 block 的對映容器
1 2 3 4 5 6 7 8 9 10 11 |
__block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ // dispatch_barrier_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); }); } |
在執行進行中 block、完成 block 的時候,都會使用以上這幾行程式碼。其作用是為了維護一個字典,key 為圖片的唯一標識 url ,值為一個 block 的陣列,來統一管理這些回撥方法。其大致的結構圖如下表示:
(圖片來源:polobymulberry)
執行過程中的 block 的時候,在初始化字典管理的時候使用了 dispatch_sync
同步執行操作,而沒有增加柵欄函式(註釋中為增加柵欄函式)。但在對於完成 block 的管理時,為了保證執行緒安全的競爭選擇問題,SD 作者選用了柵欄函式對執行緒進行了先後執行的規定。為什麼這裡不用柵欄呢?筆者的理解如下:由於這兩個位置,都是對於 URLCallbacks
的讀寫操作,而在這之前是沒有任何更新 URLCallbacks
的操作,所以不需要設定柵欄,只需要同步繼續即可。而對於柵欄函式,是用在非同步操作中對於操作順序進行控制,由於 SD 需要支援多圖片同時下載,所以需要在每次的 URLCallbacks
寫資料結束後,再進行讀操作。
NSMutableURLRequest 網路請求
1 2 3 |
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; |
initWithURL
的作用是根據 url 、快取策略(Cache Policy)、下載最大時限(Time Out Interval)來產生一個 NSURLRequest
。先來看下快取策略的選擇:
- SDWebImageDownloaderUseNSURLCache:在 SDWebImage 中,預設條件下,請求是不使用
NSURLCache
的。如果使用該選項,NSURLCache
就應該使用預設的快取策略NSURLRequestUseProtocolCachePolicy
。 - NSURLRequestUseProtocolCachePolicy:對特定 url 請求使用網路協議(例如 HTTP)中實現的快取邏輯。這是一個預設的策略。該策略表示如果快取不存在,直接從服務端獲取。如果快取存在,會根據 Response 中的 Cache-Control 欄位判斷下一步操作。例如:當 Cache-Control 欄位為 must-revalidata ,則會詢問服務端該資料是否有更新,無更新則返回給使用者快取資料,若已經更新,則請求伺服器以獲取最新資料。
- NSURLRequestReloadIgnoringLocalCacheData:資料需要從原始地址(一般就是重新從伺服器獲取)。不使用現有快取。
1 2 3 4 5 6 7 8 9 10 11 |
// 要點一 request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); // 要點二 request.HTTPShouldUsePipelining = YES; // 要點三 if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; } |
後面就是對於 request 的一些屬性的設定,從屬性名上可以看出使用的是 HTTP 協議:
- 要點一:
HTTPShouldHandleCookies
如果設定為 YES,在處理時我們直接查詢NSHTTPCookieStore
中的 cookies 即可。HTTPShouldHandleCookies
這個策略表示是否應該給 Request 設定 cookie 並伴隨著 Request 一起傳送出去。然後 Response 返回的 cookie 會繼續根據訪問策略(Cookie Acceptance Policy)接收到系統中。 - 要點二:
HTTPShouldUsePipelining
表示 receiver (常常理解為 client 客戶端)的下一個資訊是否必須等到上一個請求回覆才能傳送。如果為 YES 表示可以, NO 反之。這個就是我們常常提到的 HTTP 管線化(HTTP Pipelining),如此可以顯著降低請求的載入時間。 - 要點三:
headersFilter
是使用自定義方法來設定 HTTP 的 Head Filed。這裡可以看下 HTTPHeader 的初始化(下載 webp 圖片與通常情況下的 header 不同):
1 2 3 4 5 |
#ifdef SD_WEBP _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; #else _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy]; #endif |
NSURLCredential 身份認證
web 服務可以在返回 HTTP 響應時附帶認證要求的 Challenge,作用是詢問 HTTP 請求的發起方是誰,這時候發起方應提供正確的使用者名稱和密碼(認證資訊),然後 web 服務才會返回真正的 HTTP 響應。
收到認證要求時,
NSURLConnection
的委託物件會收到相應的訊息並得到一個NSURLAuthenticationChallenge
例項。該例項的傳送方遵守NSURLAuthenticationChallengeSender
協議。為了繼續收到真實的資料,需要向該傳送方向發回一個NSURLCredential
例項。
1 2 3 4 5 6 7 |
if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } |
當已經有用 NSURLCredential
,則直接使用,沒有的話則重新構建一個例項並儲存下來。NSURLCredential
在其中的作用就是快取對於證書的授權處理。這是對於 https 協議而言,如果想了解更多建議閱讀 Foundation的官方文件。
總結
在 Downloader 中,主要的操作就是用於組織一個 URLCallbacks
字典,用於管理圖片指定的進行 block 、完成 block。並且,在 downloadImageWithURL:
方法中,Downloader 其實一直在更新一個 operation 並作為返回值。所以,Downloader 的主要作用是實現多圖片非同步下載請求,並將其封裝為一個 operation 提交給上層統一管理。
下一篇主要講解一下 DownloaderOperation 下載操作任務管理。