前言:
本文由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,需要移除然後調用。這個是為了告訴系統我們已經完成了,可以處理新的下載下傳任務了。