天天看點

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,有充足的電量。這個屬性主要針對背景回話的,是以在背景會話模式下預設是打開的。

當我們建立了一個會話配置對象後,就可以用它來建立會話對象了:

NSURLSession *inProcessSession;
inProcessSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];      

注意:這裡我們把自己設定為代理了。通過代理方法可以告訴我們資料傳輸進度以及擷取認證資訊。下面我們會實作一些合适的代理。

資料傳輸時封裝在任務裡面的,這裡有三種類型:

1. 資料任務 (NSURLSessionDataTask)

2. 上傳任務 (NSURLSessionUploadTask)

3. 下載下傳任務(NSURLSessionDownloadTask)

在會話中傳輸資料時,我們需要實作某一種任務。比如下載下傳:

NSString *url = @"http://appropriate/url/here";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; NSURLSessionDownloadTask *cancellableTask = [inProcessSession downloadTaskWithRequest:request];
[cancellableTask resume];      

現在會話将會異步下載下傳此url的檔案内容。

我們需要實作一個代理方法來擷取這個下載下傳的内容:

-        (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
-        {
-         // We've successfully finished the download. Let's save the file
-        NSFileManager *fileManager = [NSFileManager defaultManager];
-        NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
-        NSURL *documentsDirectory = URLs[0];
-        NSURL *destinationPath = [documentsDirectory URLByAppendingPathComponent:[location lastPathComponent]];
-        NSError *error;
-        // Make sure we overwrite anything that's already there
-        [fileManager removeItemAtURL:destinationPath error:NULL];
-        BOOL success = [fileManager copyItemAtURL:location toURL:destinationPath error:&error]; if (success)
-         {
-        dispatch_async(dispatch_get_main_queue(), ^{
-        UIImage *p_w_picpath = [UIImage p_w_picpathWithContentsOfFile:[destinationPath path]]; self.p_w_picpathView.p_w_picpath = p_w_picpath;
-        self.p_w_picpathView.contentMode = UIViewContentModeScaleAspectFill;
-        self.p_w_picpathView.hidden = NO; });
-        }
-        else
-        {
-         NSLog(@"Couldn't copy the downloaded file");
-        }
-        if(downloadTask == cancellableTask)
-        {
-        cancellableTask = nil;
-         }
-        }      

這個方法在NSURLSessionDownloadTaskDelegate代理中。在代碼中,我們擷取到下載下傳檔案的臨時目錄,并把它儲存到文檔目錄下(因為有個圖檔),然後顯示給使用者。

上面的代理是下載下傳成功的回調方法。下面代理方法也在NSURLSessionDownloadTaskDelegate代理中,不管任務是否成功,在完成後都會回調這個代理方法。

-        (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
-        {
-        dispatch_async(dispatch_get_main_queue(), ^{ self.progressIndicator.hidden = YES; });
-         }      

如果error是nil,則證明下載下傳是成功的,否則就要通過它來查詢失敗的原因。如果下載下傳了一部分,這個error會包含一個NSData對象,如果後面要恢複任務可以用到。

傳輸進度

上一節結尾,你可能注意到我們有一個進度來标示每個任務完成度。更新進度條可能不是很容易,會有一個額外的代理來做這件事情,當然它會被調用多次。

-        (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten BytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
-        {
-        double currentProgress = totalBytesWritten / (double)totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^{
-        self.progressIndicator.hidden = NO; self.progressIndicator.progress = currentProgress; });
-        }      

這是NSURLSessionDownloadTaskDelegate的另一個代理方法,我們用來計算進度并更新進度條。

取消下載下傳

NSURLConnection一旦發送是沒法取消的。但是,我們可以很容易的取消掉一個NSURLSessionTask任務:

-        (IBAction)cancelCancellable:(id)sender
-        {
-         if(cancellableTask)
-        {
-         [cancellableTask cancel];
-        cancellableTask = nil;
-        }
-        }      

非常容易!當取消後,會回調這個URLSession:task:didCompleteWithError:代理方法,通知你去及時更新UI。當取消一個任務後,也十分可能會再一次回調這個代理方法URLSession:downloadTask:didWriteData:BytesWritten:totalBytesExpectedToWrite: 。當然,didComplete 方法肯定是最後一個回調的。

恢複下載下傳

恢複下載下傳也非常容易。這裡重寫了個取消方法,會生成一個NSData對象,可以在以後用來繼續下載下傳。如果伺服器支援恢複下載下傳,這個data對象會包含已經下載下傳了的内容。

-        (IBAction)cancelCancellable:(id)sender
-        {
-        if(self.resumableTask)
-        {
-        [self.resumableTask cancelByProducingResumeData:^(NSData *resumeData)
-        {
-         partialDownload = resumeData; self.resumableTask = nil; }];
-        }
-        }      

上面方法中,我們把待恢複的資料儲存到一個變量中,友善後面恢複下載下傳使用。

當新建立一個下載下傳任務的時候,除了使用一個新的請求,我們也可以使用待恢複的下載下傳資料:

if(!self.resumableTask)
 {
if(partialDownload)
{
self.resumableTask = [inProcessSession downloadTaskWithResumeData:partialDownload];
 }
 else
 {
 NSString *url = @"http://url/for/p_w_picpath";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.resumableTask = [inProcessSession downloadTaskWithRequest:request];
}
[self.resumableTask resume];
 }      

如果我們有這個partialDownload這個資料對象,就可以用它來建立一個新的任務。如果沒有,就按以前的步驟來建立任務。

記住:當使用partialDownload建立任務完成後,需要把partialDownload設定為nil。

背景下載下傳

NSURLSession另一個重要的特性:即使當應用不在前台時,你也可以繼續傳輸任務。當然,我們的會話模式也要為背景模式:

-        (NSURLSession *)backgroundSession
-        {
-        static NSURLSession *backgroundSession = nil;
-        static dispatch_once_t onceToken;
-        dispatch_once(&onceToken, ^{
-        NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.shinobicontrols.BackgroundDownload.BackgroundSession"];
-        backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; });
-        return backgroundSession;
-         }      

需要非常注意的是,通過給的背景token,我們隻能建立一個背景會話,是以這裡使用dispatch once block。token的目的是為了當應用重新開機後,我們可以通過它擷取會話。建立一個背景會話,會啟動一個背景傳輸守護程序,這個程序會管理資料并傳輸給我們。即使當應用挂起或者終止,它也會繼續運作。

開啟背景下載下傳任務和之前一樣,所有的背景功能都是NSURLSession自己管理的。

NSString *url = @"http://url/for/picture";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.backgroundTask = [self.backgroundSession downloadTaskWithRequest:request]; [self.backgrounTask resume];      

現在,即使你按home鍵離開應用,下載下傳也會在背景繼續(受開始提到的配置項控制)。

當下載下傳完成後,你的應用将被重新開機,并傳輸内容過來。

将會調用app delegate的這個方法:

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
self.backgroundURLSessionCompletionHandler = completionHandler;
}      

這裡,我們擷取内容通過completionHandler,當我們接收下載下傳的資料并更新UI時會調用completionHandler。我們儲存了completionHandler(注意需要copy),讓正在加載的View Controller來處理資料。當View Controller加載成功後,建立背景會話(并設定代理)。是以之前使用的相同代理方法就會被調用。

-        (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
-        { // Save the file off as before, and set it as an p_w_picpath view//...
-        if (session == self.backgroundSession)
-        {
-        self.backgroundTask = nil;
-        // Get hold of the app delegate
-        SCAppDelegate *appDelegate = (SCAppDelegate *)[[UIApplication sharedApplication] delegate];
-         if(appDelegate.backgroundURLSessionCompletionHandler)
-         { // Need to copy the completion handlervoid (^handler)() = appDelegate.backgroundURLSessionCompletionHandler; appDelegate.backgroundURLSessionCompletionHandler = nil;
-         handler();
-         }
-         }
-         }      

需要注意的幾個地方:

1. 不能用downloadTask和self.backgroundTask來比較。因為我們不能确定self.backgroundTask是不是已經有了,有可能是應用新的一次重新開機。比較session是可行的。

2. 這裡使用app delegate來擷取completion handler 的。其實,有很多方式來擷取completion handler 的。

3. 當儲存完檔案并顯示完成後,如果有completion handler,需要移除然後調用。這個是為了告訴系統我們已經完成了,可以處理新的下載下傳任務了。

繼續閱讀