前言:
本文由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,有充足的电量。这个属性主要针对后台回话的,因此在后台会话模式下默认是打开的。
当咱们建立了一个会话配置对象后,就能够用它来建立会话对象了:
php
1 |
NSURLSession *inProcessSession; |
2 |
inProcessSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; |
注意:这里咱们把本身设置为代理了。经过代理方法能够告诉咱们数据传输进度以及获取认证信息。下面咱们会实现一些合适的代理。
数据传输时封装在任务里面的,这里有三种类型:
1. 数据任务 (NSURLSessionDataTask)
2. 上传任务 (NSURLSessionUploadTask)
3. 下载任务(NSURLSessionDownloadTask)
在会话中传输数据时,咱们须要实现某一种任务。好比下载:
ios
2 |
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; NSURLSessionDownloadTask *cancellableTask = [inProcessSession downloadTaskWithRequest:request]; |
3 |
[cancellableTask resume]; |
如今会话将会异步下载此url的文件内容。
咱们须要实现一个代理方法来获取这个下载的内容:
git
01 |
- ( void )URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location |
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]]; |
10 |
- [fileManager removeItemAtURL:destinationPath error:NULL]; |
11 |
- BOOL success = [fileManager copyItemAtURL:location toURL:destinationPath error:&error]; if (success) |
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; }); |
20 |
- NSLog(@ "Couldn't copy the downloaded file" ); |
22 |
- if (downloadTask == cancellableTask) |
24 |
- cancellableTask = nil; |
这个方法在NSURLSessionDownloadTaskDelegate代理中。在代码中,咱们获取到下载文件的临时目录,并把它保存到文档目录下(由于有个图片),而后显示给用户。
上面的代理是下载成功的回调方法。下面代理方法也在NSURLSessionDownloadTaskDelegate代理中,无论任务是否成功,在完成后都会回调这个代理方法。
github
1 |
- ( void )URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error |
3 |
- dispatch_async(dispatch_get_main_queue(), ^{ self.progressIndicator.hidden = YES; }); |
若是error是nil,则证实下载是成功的,不然就要经过它来查询失败的缘由。若是下载了一部分,这个error会包含一个NSData对象,若是后面要恢复任务能够用到。
传输进度
上一节结尾,你可能注意到咱们有一个进度来标示每一个任务完成度。更新进度条可能不是很容易,会有一个额外的代理来作这件事情,固然它会被调用屡次。
安全
1 |
- ( void )URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten BytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite |
3 |
- double currentProgress = totalBytesWritten / ( double )totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^{ |
4 |
- self.progressIndicator.hidden = NO; self.progressIndicator.progress = currentProgress; }); |
这是NSURLSessionDownloadTaskDelegate的另外一个代理方法,咱们用来计算进度并更新进度条。
取消下载
NSURLConnection一旦发送是无法取消的。可是,咱们能够很容易的取消掉一个NSURLSessionTask任务:
服务器
1 |
- (IBAction)cancelCancellable:(id)sender |
5 |
- [cancellableTask cancel]; |
6 |
- cancellableTask = nil; |
很是容易!当取消后,会回调这个URLSession:task:didCompleteWithError:代理方法,通知你去及时更新UI。当取消一个任务后,也十分可能会再一次回调这个代理方法URLSession:downloadTask:didWriteData:BytesWritten:totalBytesExpectedToWrite: 。固然,didComplete 方法确定是最后一个回调的。
恢复下载
恢复下载也很是容易。这里重写了个取消方法,会生成一个NSData对象,能够在之后用来继续下载。若是服务器支持恢复下载,这个data对象会包含已经下载了的内容。
cookie
1 |
- (IBAction)cancelCancellable:(id)sender |
3 |
- if (self.resumableTask) |
5 |
- [self.resumableTask cancelByProducingResumeData:^(NSData *resumeData) |
7 |
- partialDownload = resumeData; self.resumableTask = nil; }]; |
上面方法中,咱们把待恢复的数据保存到一个变量中,方便后面恢复下载使用。
当新建立一个下载任务的时候,除了使用一个新的请求,咱们也可使用待恢复的下载数据:网络
01 |
if (!self.resumableTask) |
05 |
self.resumableTask = [inProcessSession downloadTaskWithResumeData:partialDownload]; |
10 |
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.resumableTask = [inProcessSession downloadTaskWithRequest:request]; |
12 |
[self.resumableTask resume]; |
若是咱们有这个partialDownload这个数据对象,就能够用它来建立一个新的任务。若是没有,就按之前的步骤来建立任务。
记住:当使用partialDownload建立任务完成后,须要把partialDownload设置为nil。
后台下载
NSURLSession另外一个重要的特性:即便当应用不在前台时,你也能够继续传输任务。固然,咱们的会话模式也要为后台模式:
session
1 |
- (NSURLSession *)backgroundSession |
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; |
须要很是注意的是,经过给的后台token,咱们只能建立一个后台会话,因此这里使用dispatch once block。token的目的是为了当应用重启后,咱们能够经过它获取会话。建立一个后台会话,会启动一个后台传输守护进程,这个进程会管理数据并传输给咱们。即便当应用挂起或者终止,它也会继续运行。
开启后台下载任务和以前同样,全部的后台功能都是NSURLSession本身管理的。
app
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 |
3 |
self.backgroundURLSessionCompletionHandler = completionHandler; |
这里,咱们获取内容经过completionHandler,当咱们接收下载的数据并更新UI时会调用completionHandler。咱们保存了completionHandler(注意须要copy),让正在加载的View Controller来处理数据。当View Controller加载成功后,建立后台会话(并设置代理)。所以以前使用的相同代理方法就会被调用。
01 |
- ( void )URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location |
03 |
- if (session == self.backgroundSession) |
05 |
- self.backgroundTask = nil; |
07 |
- SCAppDelegate *appDelegate = (SCAppDelegate *)[[UIApplication sharedApplication] delegate]; |
08 |
- if (appDelegate.backgroundURLSessionCompletionHandler) |
须要注意的几个地方:
1. 不能用downloadTask和self.backgroundTask来比较。由于咱们不能肯定self.backgroundTask是否是已经有了,有多是应用新的一次重启。比较session是可行的。
2. 这里使用app delegate来获取completion handler 的。其实,有不少方式来获取completion handler 的。
3. 当保存完文件并显示完成后,若是有completion handler,须要移除而后调用。这个是为了告诉系统咱们已经完成了,能够处理新的下载任务了。