無論是社交類的網絡相冊、電商類的商品清單、還是電子書城的書架等應用場景,大量圖片的下載都是必備的應用需求。
在iOS系統中,相比 NSURLConnection ,NSURLSession 提供了一套更優秀的網絡處理解決方案,並且使用的接口更加簡單,對開發者更加友好。那麼 NSURLSession 能勝任多任務下載嗎?
NSURLSession 多任務的侷限性
NSURLSession 本身能處理多任務下載, 我們可以使用dataTaskWithURL方法爲每個URL發起請求:
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSLog(@"Handle response"); // 斷點位置
}] resume];
可見,NSURLSession 內部開闢NSOperationQueue 來處理多任務,NSURLSession 的 block 使用簡單,但能實現的功能有限,所以,要想控制下載的過程,就必須使用代理:
// NSURLSessionDataDelegate
- URLSession:dataTask:didReceiveResponse:completionHandler: //接收到服務器的響應
- URLSession:dataTask:didReceiveData: //接收到服務器的數據
- URLSession:dataTask:willCacheResponse:completionHandler: //指定緩存數據
// NSURLSessionDataDelegate
- URLSession:task:didCompleteWithError: //下載完成
- URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: //重定向
然而,對一個NSURLSession而言,所有任務都會走同一個代理方法,導致代理中需要編寫許多區分不同任務的代碼,爲防止公共屬性訪問衝突,需要對處理過程加鎖,這導致了性能的下降;
當然,有人會說,每個任務用不同的 NSURLSession 就好了,誠然,這可以解決代理複用的問題,但正如前面提到的,每個NSURLSession自己會維護一個NSOperationQueue,這種處理方法相當於爲每個下載任務開闢了一個NSOperationQueue,卻只放入一個操作對象,不但代碼非常彆扭,在大量任務時會增加系統的開銷。
那麼,該如何有效地將任務分開,分別控制下載進程,優雅的處理多任務下載呢?
SDWebImage的解決方法
SDWebImage 作爲GitHub 獲得Star 20K+ 的優秀庫,在實現了大量圖片異步下載、緩存、解碼一系列功能的同時,提供了簡潔的使用接口,達到了優異的性能,它內部也是使用NSURLSession下載圖片的,那麼它是如何控制NSURLSession的呢?讓我們到SDWebImage的源碼中一探究竟。
下載相關的類
SDWebImage 中負責下載的主要有兩個類:
- SDWebImageDownloader 封裝了 NSURLSession ,同時作爲 Session 的代理,並初始化了一個 NSOperationQueue ,管理所有的下載過程。
- SDWebImageDownloaderOperation 實現了一張圖片的整個下載過程,通過 task 啓動下載任務,開闢新線程處理下載到的圖片等等。
它們的關係如下圖,實心箭頭表示持有關係,虛線表示間接使用:
下載的主要過程解析
下載入口
下載的過程首先從 Downloader 的 downloadImage 方法開始:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
若URL未被下載過,一個 SDWebImageDownloaderOperation 就會被創建,並添加到 downloadQueue 中,由隊列負責執行,這時,還沒實際下載,只有Operation 被提交執行,纔會開始真正的下載。
啓動下載
接着來到 SDWebImageDownloaderOperation 中看看下載的啓動 :
下載的啓動過程放在了 start 方法中,而不是 main 方法,這是因爲main方法執行完成後,會自行退出,無法控制線程的生命週期。
在 star 方法中,一個 NSURLSessionTask 實例被創建,並通過以下方法啓動起來:
[self.dataTask resume];
這時,實際的下載過程是在NSURLSession的隊列中進行的,SDWebImageDownloaderOperation 的線程處於【等待】的狀態中。
下載完成
下載完成後,在 SDWebImageDownloader 中獲得NSURLSessionTaskDelegate 回調:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
在該代理方法中,通過遍歷 self.downloadQueue.operations 查詢 NSURLSessionTask 對應的 SDWebImageDownloaderOperation,調用 Operation 實現的同名代理方法URLSession:dataTask:didReceiveResponse:completionHandler:
,將執行的過程切回到 Operation 中,在這裏完成最後的收尾工作,調用
[self done];
需要注意的是,以下屬性的 setter 方法:
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
自定義實現併發操作對象時,必須覆蓋這些屬性的實現,以便可以返回操作的狀態,並且必須爲這些鍵路徑生成KVO通知(即setter中的willChangeValueForKey
和didChangeValueForKey
),否則會造成 NSOperation 無法退出。
到這裏就將一張圖片下載的主要過程分析完了,其他網絡錯誤處理、緩存處理過程,可以沿着這個流程進一步加以分析,這裏限於篇幅就不展開了。
NSOperation vs GCD
這裏補充一點的是,在 Operation 過程中,切到主線程發通知、調用解碼器等,都使用GCD開闢新線程,從而釋放出 NSURLSession 的下載線程,這是比較有意思的部分,NSOperation 內部的具體實現是用GCD的
,體現了NSOperation 和 GCD 的區別與聯繫:
- NSOperation 用於控制複雜過程的生命週期;
- GCD 用於快速執行異步操作;
- 兩者都是啓用多線程的手段,可以協同工作。
總結
SDWebImage的下載方案中的優點:
- 異步下載圖片,開闢多個線程處理各階段過程,使主線程不卡頓
- 靈活使用 NSOperation 和 GCD,使得代碼條理清晰