天天品尝iOS7甜点 :: Day 1 :: NSURLSession

这篇文章是天天品尝iOS7甜点系列的一部分,你可以查看完整的系列目录:天天品尝iOS7甜点


在过去的网络环境中我们都是用全局的状态NSURLConnection来管理cookies和authentication, 因此会出现两个不同的连接互相竞争共享的设置,NSURLSession着重就是解决这些问题和其他的一些问题的。

伴随着这个指南,我们讨论三个不同的下载情形.这篇文章不会描述所有的项目,只是突出一些与NSURLSession API相关的内容。

完成的代码已经在github可供下载使用:github.com/ShinobiControls/iOS7-day-by-day

简单下载

NSURLSession代表了多重连接所有相关的状态,在以前是一个共享的全局状态。会话对象通过一个配置对象的工厂方法进行创建.

有3中可能的session:

  1. 默认的,进程内会话
  2. 短暂的(内存内),进程内会话
  3. 后台会话

为了一个简单的下载我们将会使用一个默认的会话:

1
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];

一旦一个配置的对象已经被创建,则它具有很多的属性可以控制它的行为。例如:它可以设置TLS安全的访问级别,不管cookies是否允许或者超时,其中两个令人关注的属性是allowsCellularAccessdiscretionary.前者指定设备在蜂窝网络下面是否允许进行网络会话。设置一个会话,可以在合适的时间内允许操作系统进行网络的访问,(如WIKI已经连接上),并且让设备更加的节能。最主要的意图就是用在后台进程会话中,设置默认值为true.

如果我们有一个会话的配置对象,我们就可以创建自身会话:

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

注意,我们设置了自身为代理,代理的方法用来通知我们数据传输的进度和请求信息进行用户验证,我们将很快实现这些方法.

在任务中数据传输都是加密的,传输有3种类型:

  1. 数据任务(NSURLSessionDataTask)
  2. 上传任务(NSURLSessionUploadTask)
  3. 下载任务(NSURLSessionDownloadTask)

为了使用会话来执行一个传输任务,我们需要创建一个任务,下面是一个简单的文件下载:

1
2
3
4
5
NSString *url = @"http://appropriate/url/here";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];

NSURLSessionDownloadTask *cancellableTask = [inProcessSession downloadTaskWithRequest:request];
[cancellableTask resume];

就是这么多,然后会话就会异步的下载指定URL的文件。

为了能够了解到request的状态信息,我们需要实现一个代理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadintToURL:(NSURL *)location {
// we've successfully finished the edownload. 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 lastPatchComponent]];
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 *image = [UIImage imageWithContentsOfFile:[destinationPath path]];
self.imageView.image = image;
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.hidden = NO;
});
}else {
NSLog(@"Couldn't copy the downloaded file");
}

if (downloadTask == cancellableTask) {
cancellableTask = nil;
}
}

这个方法定义在NSURLSessionDownloadTaskDelegate中,我们把下载的临时文件转换成下载文件,保存到文档(document)目录下面,然后得到一个图像文件,再展示给用户。

上述代理的方式只有在下载任务运行成功的时候才执行,接下来的方法是在每一个任务结束的时候执行-NSURLSessionDelegate,不管任务是不是正常结束:

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

如果error为空就表示任务是正确运行结束的,否则就需要是查找异常问题了。如果部分的下载完成后错误对象中包含一个引用的NSData对象,则可以通过它在后面的阶段恢复传输.

追终任务进度

你应该已经发现了,在上面一节最后部门,我们在任务完成后隐藏了进度指示器,动态更新这个进度条并不是十分简单,还需要附加的代理方法来处理任务的生命周期。

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

这是NSURLSessionDownloadTaskDelegate代理中的另外一个方法,我们使用它来更新进度指示器的进度。

取消一个下载

一旦一个NSURLConnection已经发送请求,则它就不可能取消。但是在NSURLSessionTask中进行取消是非常简单的:

1
2
3
4
5
6
- (IBAction)cancelCancellable:(id)sender {
if (cancellableTask) {
[cancellableTask cancel];
cancellableTask = nil;
}
}

就是这么的简单,但是值得注意的就是一旦一个任务取消了就会调用URLSession:task:didCompleteWithError:这个方法,让你能够重新更新UI的行为,然后就会取消下载的任务,URLSession:downloadTask:didWriteData:BytesWritten:totalBytesExpectedToWrite:这个方法会被再次的调用,然而didComplete方法会继续运行。

重新启动下载

重新启动下载任务也是相当的简单。在取消任务的时候提供了一个NSData对象,可以用它来开启一个新的任务,继续进行传输。如果服务器支持恢复下载,然后数据对象将包含已经下载的字节数:

1
2
3
4
5
6
7
8
- (IBAction)cancelCancellable:(id)sender {
if (self.resumableTask) {
[self.resumableTask cancelByProductingResumeData:^(NSData *resumeData) {
partialDownload = resumeData;
self.resumableTask = nil;
}];
}
}

这个我们把恢复的数据保存到一个类变量中,这样我们就可以在以后使用它再次进行下载了。当我们重新创建一个下载的时候,而不是提供一个请求,可以提供已经保存的数据对象。

1
2
3
4
5
6
7
8
9
10
if (!self.resumableTask) {
if (partialDownload) {
self.resumableTask = [inProcessSession downloadTaskWithResumeData:partialDownload];
}else {
NSString *url = @"http://url/for/image";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
self.resumableTask = [inProcessSession downloadTaskWithRequest:request];
}
[self.resumableTask resume];
}

如果我们已经保存了已下载的部分内容,我们就可以用它来生成一个新的下载会话,否则的话,就需要重新创建一个新的下载会话。

唯一需要注意的就是记得在任务完成以后,需要设置partialDownload = nil

后台下载

NSURLSession另外一个主要的特点就是当app不在运行的时候依然可以进行数据传输。为了达到这个目的,我们需要把会话(session)设置成后台会话:

1
2
3
4
5
6
7
8
9
- (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;
}

开启一个后台下载任务和我们之前的做法一致,所有的’后台’功能都被我们刚刚创建的NSURLSession管理了:

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

现在,即使你关闭了应用程序,这个下载任务依然在后台运行。

当这个下载任务完成了,然后iOS将会重新启动app来让任务知道,对这个我们不必放在心上。为了达到这个目的,我们可以在app的代理中调用下面的代码:

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

这里我们进行统一的处理下载的数据和更新UI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
// Save the file off as before, and set it as an image view
// ...

if (session == self.backgroundSession) {
self.backgroundTask = nil;
// Get hold of the app delegate
SCAppDelegate *appDelegate = (SCAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDeledate.backgroundURLSessionCompletionHandler) {
// Need to copy the completion handler
void (^handler)() = appDelegate.backgroundURLSessionCompletionHandler;
appDelegate.backgroundURLSessionCompletionHandler = nil;
handler();
}
}
}

这里有些事情需要注意一下:

  • 我们不能够比较downloadTaskself.backgroundTask,因为我们不能够保证self.backgroundTask是否已经被赋值了(由于这是一个新启动的app),使用session比较是有效的。
  • 我们使用app的delegate(代理), 还有其他的方式来放置handler。
  • 一旦我们保存文件结束,我们需要把完成的handler删除掉,然后运行它,这就告诉操作系统我们已经处理完新的下载。

总结

NSURLSession提供了很多新的有价值的特性来处理iOS和OSX 10.9中的网络问题,可以用来替代原有的旧的处理方式。在最新的操作系统中都可以来使用它。

本文翻译自:iOS7 Day-by-Day :: Day 1 :: NSURLSession

文章目录
  1. 1. 简单下载
  2. 2. 追终任务进度
  3. 3. 取消一个下载
  4. 4. 重新启动下载
  5. 5. 后台下载
  6. 6. 总结
,