源碼啓示錄 | 如何使用NSURLSession下載海量圖片

無論是社交類的網絡相冊、電商類的商品清單、還是電子書城的書架等應用場景,大量圖片的下載都是必備的應用需求。
在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中的willChangeValueForKeydidChangeValueForKey),否則會造成 NSOperation 無法退出。

到這裏就將一張圖片下載的主要過程分析完了,其他網絡錯誤處理、緩存處理過程,可以沿着這個流程進一步加以分析,這裏限於篇幅就不展開了。

NSOperation vs GCD

這裏補充一點的是,在 Operation 過程中,切到主線程發通知、調用解碼器等,都使用GCD開闢新線程,從而釋放出 NSURLSession 的下載線程,這是比較有意思的部分,NSOperation 內部的具體實現是用GCD的,體現了NSOperation 和 GCD 的區別與聯繫:

  • NSOperation 用於控制複雜過程的生命週期;
  • GCD 用於快速執行異步操作;
  • 兩者都是啓用多線程的手段,可以協同工作。

總結

SDWebImage的下載方案中的優點:

  • 異步下載圖片,開闢多個線程處理各階段過程,使主線程不卡頓
  • 靈活使用 NSOperation 和 GCD,使得代碼條理清晰
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章