iOS7新特性-NSURLSession詳解

前言:
本文由DevDiv版主 @jas 原創翻譯,轉載請註明出處!
原文: http://www.shinobicontrols.com/b … day-1-nsurlsession/
大家都知道,過去的IOS系統網絡處理是通過NSURLConnection來實現的。由於NSURLConnection通過全局狀態來管理cookies和認證信息,這就意味着在某種情況下,可能同時存在兩個不同的連接去使用這些公共資源。NSURLSession很好的解決了許多這種類似的問題。
本文連同附件一共討論了三種不同的下載場景。本文會着重講述有關NSURLSession的部分,整個項目就不再闡述了。代碼可以在github回購。
NSURLSession狀態同時對應着多個連接,不像之前使用共享的一個全局狀態。會話是通過工廠方法來創建配置對象。
總共有三種會話:
1. 默認的,進程內會話
2. 短暫的(內存),進程內會話
3. 後臺會話
如果是簡單的下載,我們只需要使用默認模式即可:
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
配置對象有很多屬性。例如,可以設置TLS安全等級,TLS決定你可以使用cookies和超時時間。還有兩個非常有趣的屬性:allowsCellularAccess和discretionary。前一個屬性表示當只有一個3G網絡時,網絡是否允許訪問。設置discretionary屬性可以控制系統在一個合適的時機訪問網絡,比如有可用的WiFi,有充足的電量。這個屬性主要針對後臺回話的,所以在後臺會話模式下默認是打開的。
當我們創建了一個會話配置對象後,就可以用它來創建會話對象了:
1 NSURLSession *inProcessSession;
2 inProcessSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];

注意:這裏我們把自己設置爲代理了。通過代理方法可以告訴我們數據傳輸進度以及獲取認證信息。下面我們會實現一些合適的代理。
數據傳輸時封裝在任務裏面的,這裏有三種類型:
1. 數據任務 (NSURLSessionDataTask)
2. 上傳任務 (NSURLSessionUploadTask)
3. 下載任務(NSURLSessionDownloadTask)
在會話中傳輸數據時,我們需要實現某一種任務。比如下載:
1 NSString *url = @ “http://appropriate/url/here” ;
2 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; NSURLSessionDownloadTask *cancellableTask = [inProcessSession downloadTaskWithRequest:request];
3 [cancellableTask resume];

現在會話將會異步下載此url的文件內容。
我們需要實現一個代理方法來獲取這個下載的內容:
01 - ( void )URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didFinishDownloadingToURL:(NSURL *)location
02 - {
03 - // We’ve successfully finished the download. Let’s save the file
04 - NSFileManager *fileManager = [NSFileManager defaultManager];
05 - NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
06 - NSURL *documentsDirectory = URLs[0];
07 - NSURL *destinationPath = [documentsDirectory URLByAppendingPathComponent:[location lastPathComponent]];
08 - NSError *error;
09 - // Make sure we overwrite anything that’s already there
10 - [fileManager removeItemAtURL:destinationPath error:NULL];
11 - BOOL success = [fileManager copyItemAtURL:location toURL:destinationPath error:&error]; if (success)
12 - {
13 - dispatch_async(dispatch_get_main_queue(), ^{
14 - UIImage *image = [UIImage imageWithContentsOfFile:[destinationPath path]]; self.imageView.image = image;
15 - self.imageView.contentMode = UIViewContentModeScaleAspectFill;
16 - self.imageView.hidden = NO; });
17 - }
18 - else
19 - {
20 - NSLog(@ “Couldn’t copy the downloaded file” );
21 - }
22 - if (downloadTask == cancellableTask)
23 - {
24 - cancellableTask = nil;
25 - }
26 - }

這個方法在NSURLSessionDownloadTaskDelegate代理中。在代碼中,我們獲取到下載文件的臨時目錄,並把它保存到文檔目錄下(因爲有個圖片),然後顯示給用戶。
上面的代理是下載成功的回調方法。下面代理方法也在NSURLSessionDownloadTaskDelegate代理中,不管任務是否成功,在完成後都會回調這個代理方法。
1 - ( void )URLSession:(NSURLSession )session task:(NSURLSessionTask )task didCompleteWithError:(NSError *)error
2 - {
3 - dispatch_async(dispatch_get_main_queue(), ^{ self.progressIndicator.hidden = YES; });
4 - }

如果error是nil,則證明下載是成功的,否則就要通過它來查詢失敗的原因。如果下載了一部分,這個error會包含一個NSData對象,如果後面要恢復任務可以用到。
傳輸進度
上一節結尾,你可能注意到我們有一個進度來標示每個任務完成度。更新進度條可能不是很容易,會有一個額外的代理來做這件事情,當然它會被調用多次。
1 - ( void )URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didWriteData:(int64_t)bytesWritten BytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
2 - {
3 - double currentProgress = totalBytesWritten / ( double )totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^{
4 - self.progressIndicator.hidden = NO; self.progressIndicator.progress = currentProgress; });
5 - }

這是NSURLSessionDownloadTaskDelegate的另一個代理方法,我們用來計算進度並更新進度條。
取消下載
NSURLConnection一旦發送是沒法取消的。但是,我們可以很容易的取消掉一個NSURLSessionTask任務:
1 - (IBAction)cancelCancellable:(id)sender
2 - {
3 - if (cancellableTask)
4 - {
5 - [cancellableTask cancel];
6 - cancellableTask = nil;
7 - }
8 - }

非常容易!當取消後,會回調這個URLSession:task:didCompleteWithError:代理方法,通知你去及時更新UI。當取消一個任務後,也十分可能會再一次回調這個代理方法URLSession:downloadTask:didWriteData:BytesWritten:totalBytesExpectedToWrite: 。當然,didComplete 方法肯定是最後一個回調的。
恢復下載
恢復下載也非常容易。這裏重寫了個取消方法,會生成一個NSData對象,可以在以後用來繼續下載。如果服務器支持恢復下載,這個data對象會包含已經下載了的內容。
1 - (IBAction)cancelCancellable:(id)sender
2 - {
3 - if (self.resumableTask)
4 - {
5 - [self.resumableTask cancelByProducingResumeData:^(NSData *resumeData)
6 - {
7 - partialDownload = resumeData; self.resumableTask = nil; }];
8 - }
9 - }

上面方法中,我們把待恢復的數據保存到一個變量中,方便後面恢復下載使用。
當新創建一個下載任務的時候,除了使用一個新的請求,我們也可以使用待恢復的下載數據:
01 if (!self.resumableTask)
02 {
03 if (partialDownload)
04 {
05 self.resumableTask = [inProcessSession downloadTaskWithResumeData:partialDownload];
06 }
07 else
08 {
09 NSString *url = @ “http://url/for/image” ;
10 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.resumableTask = [inProcessSession downloadTaskWithRequest:request];
11 }
12 [self.resumableTask resume];
13 }

如果我們有這個partialDownload這個數據對象,就可以用它來創建一個新的任務。如果沒有,就按以前的步驟來創建任務。
記住:當使用partialDownload創建任務完成後,需要把partialDownload設置爲nil。
後臺下載
NSURLSession另一個重要的特性:即使當應用不在前臺時,你也可以繼續傳輸任務。當然,我們的會話模式也要爲後臺模式:
1 - (NSURLSession *)backgroundSession
2 - {
3 - static NSURLSession *backgroundSession = nil;
4 - static dispatch_once_t onceToken;
5 - dispatch_once(&onceToken, ^{
6 - NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:@ “com.shinobicontrols.BackgroundDownload.BackgroundSession” ];
7 - backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; });
8 - return backgroundSession;
9 - }

需要非常注意的是,通過給的後臺token,我們只能創建一個後臺會話,所以這裏使用dispatch once block。token的目的是爲了當應用重啓後,我們可以通過它獲取會話。創建一個後臺會話,會啓動一個後臺傳輸守護進程,這個進程會管理數據並傳輸給我們。即使當應用掛起或者終止,它也會繼續運行。
開啓後臺下載任務和之前一樣,所有的後臺功能都是NSURLSession自己管理的。
1 NSString *url = @ “http://url/for/picture” ;
2 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.backgroundTask = [self.backgroundSession downloadTaskWithRequest:request]; [self.backgrounTask resume];

現在,即使你按home鍵離開應用,下載也會在後臺繼續(受開始提到的配置項控制)。
當下載完成後,你的應用將被重啓,並傳輸內容過來。
將會調用app delegate的這個方法:
1 -( void )application:(UIApplication )application handleEventsForBackgroundURLSession:(NSString )identifier completionHandler:( void (^)())completionHandler
2 {
3 self.backgroundURLSessionCompletionHandler = completionHandler;
4 }

這裏,我們獲取內容通過completionHandler,當我們接收下載的數據並更新UI時會調用completionHandler。我們保存了completionHandler(注意需要copy),讓正在加載的View Controller來處理數據。當View Controller加載成功後,創建後臺會話(並設置代理)。因此之前使用的相同代理方法就會被調用。
01 - ( void )URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didFinishDownloadingToURL:(NSURL *)location
02 - { // Save the file off as before, and set it as an image view//…
03 - if (session == self.backgroundSession)
04 - {
05 - self.backgroundTask = nil;
06 - // Get hold of the app delegate
07 - SCAppDelegate appDelegate = (SCAppDelegate )[[UIApplication sharedApplication] delegate];
08 - if (appDelegate.backgroundURLSessionCompletionHandler)
09 - { // Need to copy the completion handlervoid (^handler)() = appDelegate.backgroundURLSessionCompletionHandler; appDelegate.backgroundURLSessionCompletionHandler = nil;
10 - handler();
11 - }
12 - }
13 - }

需要注意的幾個地方:
1. 不能用downloadTask和self.backgroundTask來比較。因爲我們不能確定self.backgroundTask是不是已經有了,有可能是應用新的一次重啓。比較session是可行的。
2. 這裏使用app delegate來獲取completion handler 的。其實,有很多方式來獲取completion handler 的。
3. 當保存完文件並顯示完成後,如果有completion handler,需要移除然後調用。這個是爲了告訴系統我們已經完成了,可以處理新的下載任務了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章