In the previous tutorial, I introduced you to NSURLSession
. I talked about the advantages it has over NSURLConnection
and how to use NSURLSession
for simple tasks, such as fetching data from a web service and downloading an image from the web. In this tutorial, we'll take a closer look at the configuration options of NSURLSession
and how to cancel and resume a download task. We've got a lot of ground to cover so let's get started.
在前一篇教程文章中,我介绍了NSURLSession。谈及了 NSURLSession 优于 NSURLConnection 的特性,并且提供了简单的例子展示了如何使用 NSURLSession:从网上获取数据和下载图像。在这篇教程中,我们将深入了解 NSURLSession 的配置选项,如何取消和恢复下载任务。开始吧!
As we saw in the previous tutorial, a session, an instance of NSURLSession
, is a configurable container for putting network requests into. The configuration of the session is handled by an instance of NSURLSessionConfiguration
.
正如在前文中所说,会话对象,NSURLSession 的实例,如同一个可配置的容器,用于装在网络请求。而会话对象的配置是由 NSURLSessionConfiguration 对象管理。
A session configuration object is nothing more than a dictionary of properties that defines how the session it is tied to behaves. A session has one session configuration object that dictates cookie, security, and cache policies, the maximum number of connections to a host, resource and network timeouts, etc. This is a significant improvement over NSURLConnection
, which relied on a global configuration object with much less flexibility.
一个会话管理对象无非就是一个属性字典,其中定义了会话中绑定的行为。一个会话对象对应一个会话管理对象,其决定了cookie,安全和高速缓存策略,最大主机连接数,资源管理,网络超时,等等。相比于 NSURLConnection 依赖于一个全局的配置对象,缺乏灵活性而言,NSURLSession 有很大的改进了。
Once a session is created and configured by a NSURLSessionConfiguration
instance, the session's configuration cannot be modified. If you need to modify a session's configuration, you have to create a new session. Keep in mind that it is possible to copy a session's configuration and modify it, but the changes have no effect on the session from which the configuration was copied.
一旦会话对象创建,并且由 NSURLSessionConfiguration 对象进行配置后,这个会话配置对象就不能被修改了。如果你想要修改会话配置对象,那么你必须重新创建一个新的会话。请记住,可以复制会话配置对象,然后修改会话配置对象,但是修改对会话没有影响。
The NSURLSessionConfiguration
class provides three factory constructors for instantiating standard configurations, defaultSessionConfiguration
,ephemeralSessionConfiguration
, and backgroundSessionConfiguration
. The first method returns a copy of the default session configuration object, which results in a session that behaves similarly to an NSURLConnection
object in its standard configuration. Altering a session configuration obtained through the defaultSessionConfiguration
factory method doesn't change the default session configuration which it's a copy of.
NSURLSessionConfiguration 类提供了三个工厂方法创建标准的配置实例对象:defaultSessionConfiguration
,ephemeralSessionConfiguration
, and backgroundSessionConfiguration
。第一个方法返回一个默认会话配置对象的副本,由此对应的会话行为和 NSURLConnection 对象的标准配置的对应行为相似。对通过 defaultSessionConfiguration 工厂方法获得的默认会话配置对象进行修改并不会有效。
A session configuration object created by invoking the ephemeralSessionConfiguration
factory method ensures that the resulting session uses no persistent storage for cookies, caches, or credentials. In other words, cookies, caches, and credentials are kept in memory. Ephemeral sessions are therefore ideal if you need to implement private browsing, something that simply wasn't possible before the introduction ofNSURLSession
.
通过 ephemeralSessionConfiguration 工厂方法创建的会话配置对象确保所产生的会话不使用持久化存储的cookie,缓存或者证书;换言之,cookie,缓存或者证书都是保存在内存中的。因此,临时会话对象非常适合实现私密浏览,这些在引入 NSURLSession 之前是不可能实现的。
The backgroundSessionConfiguration:
factory method creates a session configuration object that enables out-of-process uploads and downloads. The upload and download tasks are managed by a background daemon and continue to run even if the application is suspended or crashes. We'll talk more about background sessions later in this series.
backgroundSessionConfiguration: 工厂方法创建的会话配置对象,使得上传下载数据可以在进程之外进行。上传下载数据的任务由一个后台守护教程管理,并且在应用程序挂起或者奔溃的时候,仍在后台运行。我们将在本系列后面详细介绍背景会话。
As we saw in the previous tutorial, creating a session configuration object is simple. In the example shown below, I used the defaultSessionConfiguration
factory method to create a NSURLSessionConfiguration
instance. Configuring a session configuration object is as simple as modifying its properties as shown in the example. We can then use the session configuration object to instantiate a session object. The session object serves as a factory for data, upload, and download tasks, with each task corresponding to a single request. In the example below, we query the iTunes Search API as we did in the previous tutorial.
正如前篇教程所见,创建一个会话配置对象是简单的。在下面的例子中,我使用了 defaultSessionConfiguration 工厂方法创建 NSURLSessionConfiguration 对象。配置会话配置对象是简单的,如在下面例子中通过修改属性值即可。然后,我们通过会话配置对象实例化会话对象,会话对象作为数据请求,上传和下载任务的工厂,每一个任务都对应一个请求。在下面的例子中,我们使用上篇教程用过的查询接口。
// Create Session Configuration NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; // Configure Session Configuration [sessionConfiguration setAllowsCellularAccess:YES]; [sessionConfiguration setHTTPAdditionalHeaders:@{@"Accept": @"application/json"}]; // Create Session NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; // Send Request NSURL *url = [NSURL URLWithString:@"https://itunes.apple.com/search?term=apple&media=software"]; [[session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]); }] resume];
The example also illustrates how easy it is to add custom headers by setting theHTTPAdditionalHeaders
property of the session configuration object. The beauty of theNSURLSession
API is that every request that passes through the session is configured by the session's configuration object. Adding authentication headers to a set of requests, for example, becomes easy as pie.
这个例子说明了通过设置会话配置对象的 HTTPAdditionalHeaders 属性来自定义请求头部是容易的。NSURLSession 的 API 的美妙之处在于每一个请求都是通过会话配置对象对会话进行配置。例如,为一组请求添加身份验证的头部就变得容易了。
In the previous tutorial, I showed you how to download an image using the NSURLSession
API. However, network connections are unreliable and it happens all too often that a download fails due to a flaky network connection. Fortunately, resuming a download isn't difficult with the NSURLSession
API. In the next example, I'll show you how to cancel and resume the download of an image.
在前一篇教程中,我展示了通过 NSURLSession 的 API 去下载图像。然而网络连接是不可靠的,经常会发生由于网络连接错误而导致的下载失败。幸运的是,通过 NSURLSession 的 API 恢复下载并不困难。在下一个例子中,我将会展示如何取消和恢复图像的下载。
Before we take a closer look at resuming a download task, it is important to understand the difference between canceling and suspending a download task. It is possible to suspend a download task and resume it at a later time. Canceling a download task, however, stops the task and it isn't possible to resume it at a later time. There is one alternative, though. It is possible to cancel a download task by callingcancelByProducingResumeData:
on it. It accepts a completion handler that accepts one parameter, an NSData
object that is used to resume the download at a later time by invoking downloadTaskWithResumeData:
ordownloadTaskWithResumeData:completionHandler:
on a session object. The NSData
object contains the necessary information to resume the download task where it left off.
在开始了解恢复下载任务之前,先了解取消和暂停下载任务的区别是非常重要的。暂停下载任务,之后再恢复下载是可以的。但是取消下载任务,相当于停止了任务,这样就不可能在之后再恢复下载。然而,有一个替代的方案。通过调用cancelByProducingResumeData:
取消一个下载任务,它的完成处理程序块接收一个 NSData 对象参数,通过调用downloadTaskWithResumeData 方法并传入这个参数或者在会话对象上调用downloadTaskWithResumeData:completionHandler: 方法就可以恢复下载任务。其中 NSData 对象包含了恢复下载任务必要的信息。
Open the project we created in the previous tutorial or download it here. We start by adding two buttons to the user interface, one to cancel the download and one to resume the download. In the view controller's header file, create an outlet and an action for each button as shown below.
打开上一篇教程中的项目。我们在界面上添加两个按键,一个用于取消下载,一个用于恢复下载。在视图控制器的头文件中,为每个按键创建一个outlet和一个action,如下:
#import <UIKit/UIKit.h> @interface MTViewController : UIViewController @property(weak,nonatomic)IBOutlet UIButton *cancelButton; @property(weak,nonatomic)IBOutlet UIButton *resumeButton; @property(weak,nonatomic)IBOutlet UIImageView *imageView; @property(weak,nonatomic)IBOutlet UIProgressView *progressView; - (IBAction)cancel:(id)sender; - (IBAction)resume:(id)sender; @end
Open the project's main storyboard and add two buttons to the view controller's view. Position the buttons as shown in the screenshot below and connect each button with its corresponding outlet and action.
打开项目的storyboard,添加两个按键到视图中,同时连接到对应的outlet和action。
We'll need to do some refactoring to make everything work correctly. OpenMTViewController.m
and declare one instance variable and two properties. The instance variable, session
, will keep a reference to the session we'll use for downloading the image.
我们需要进行一些代码重构使得一切正常运行。打开 MTViewController.m 文件,声明一个实例变量和两个属性。实例变量 session 用于下载图像。
#import "MTViewController.h" @interfaceMTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate> { NSURLSession *_session; } @property(strong, nonatomic)NSURLSessionDownloadTask *downloadTask; @property(strong, nonatomic)NSData *resumeData; @end
We also need to refactor the viewDidLoad
method, but first I'd like to implement a getter method for the session. Its implementation is pretty straightforward as you can see below. We create a session configuration object using the defaultSessionConfiguration
factory method and instantiate the session object with it. The view controller serves as the session's delegate.
我们还需要对 viewDidLoad 方法进行重构,但首先我为 session 实例变量实现 getter 方法,它的实现是简单的,如下所示。我们通过 defaultSessionConfiguration 工厂方法创建一个会话配置对象,然后实例化 session 对象。这个视图控制器作为会话的委托代理。
- (NSURLSession*)session { if(!_session) { // Create Session Configuration NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; // Create Session _session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil]; } return _session; }
With the session
accessor implemented, the viewDidLoad
method becomes much simpler. We create a download task, as we did in the previous tutorial, and store a reference to the task in downloadTask
. We then tell the download task to resume
.
由于 session getter方法的实现,viewDidLoad 方法就变得简单了。我们创建一个下载任务,如上篇教程那样。然后通知下载任务 resume (此时即是开始下载任务)。
- (void)viewDidLoad { [super viewDidLoad]; // Create Download Task self.downloadTask= [self.session downloadTaskWithURL:[NSURL URLWithString:@"http://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg"]]; // Resume Download Task [self.downloadTask resume]; }
The cancel:
action contains the logic for canceling the download task we just created. If downloadTask
is not nil
, we call cancelByProducingResumeData:
on the task. This method accepts one parameter, a completion block. The completion block also takes one parameter, an instance of NSData
. If resumeData
is not nil
, we store a reference to the data object in view controller's resumeData
property.
cancel 方法中包含的处理逻辑就是取消之前创建的下载任务。如果 downloadTask (下载任务)非空,我们对下载任务调用 cancelByProducingResumeData: 方法,这个方法接收一个参数,完成处理程序块,这个程序块有一个 NSData 参数 resumeData,如果 resumeData 非空,我们就保存这个对象到视图控制器的 resumeData 属性中。
If a download is not resumable, the completion block's resumeData
parameter is nil
. Not every download is resumable so it's important to check if resumeData
is a validNSData
object.
如果一个下载任务是不可恢复的,那么完成处理程序块的 resumeData 参数就是 nil。并非所有的下载任务都是可恢复的,所以有必要检查 resumeData 是否是一个有效的 NSData 对象。
- (IBAction)cancel:(id)sender { if(!self.downloadTask) return; // Hide Cancel Button [self.cancelButton setHidden:YES]; [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) { if(!resumeData) return; [self setResumeData:resumeData]; [self setDownloadTask:nil]; }]; }
Resuming the download task after it was canceled is easy. In the resume:
action, we check if the view controller's resumeData
property is set. If resumeData
is a valid NSData
object, we tell the session
object to create a new download task and pass it theNSData
object. This is all the session
object needs to recreate the download task that we canceled in the cancel:
action. We then tell the download task to resume
and setresumeData
to nil
.
取消下载任务之后,恢复下载任务就变得简单了。在 resume action 方法中,我们先检查视图控制器的 resumeData 属性是否已置值。如果 resumeData 是一个有效的 DSData 对象,我们通知 session 会话对象创建一个新的下载任务,通过传递这个 NSData 对象。这个 NSData 包含了所有 session 需要重新创建下载任务的信息。然后通知下载任务开始,并置 resumeData 为 nil。
- (IBAction)resume:(id)sender { if(!self.resumeData) return; // Hide Resume Button [self.resumeButton setHidden:YES]; // Create Download Task self.downloadTask= [self.session downloadTaskWithResumeData:self.resumeData]; // Resume Download Task [self.downloadTask resume]; // Cleanup [self setResumeData:nil]; }
Build the project and run the application in the iOS Simulator or on a physical device. The download should start automatically. Tap the cancel button to cancel the download and tap the resume button to resume the download.
编译项目程序,并在模拟器或者真机上运行。下载任务是自动开始的,点击 cancel 取消按键可以取消下载,点击 resume 恢复按键可以恢复下载。
There are a number of details we need to take care of. First of all, the buttons shouldn't always be visible. We'll use key value observing to show and hide the buttons when necessary. In viewDidLoad
, hide the buttons and add the view controller as an observer of itself for the resumeData
and downloadTask
key paths.
这里我们还需要关注一些细节处理。首先按键不应该总是可见的。我们使用 KVO 去处理显示和隐藏按键。在 viewDidLoad 方法中,设置隐藏两个按键,并且设置视图控制器为其本身的观察者,key path 为 “resumeData” 和 “downloadTask”。
- (void)viewDidLoad { [super viewDidLoad]; // Add Observer [self addObserver:self forKeyPath:@"resumeData" options:NSKeyValueObservingOptionNew context:NULL]; [self addObserver:self forKeyPath:@"downloadTask" options:NSKeyValueObservingOptionNew context:NULL]; // Setup User Interface [self.cancelButton setHidden:YES]; [self.resumeButton setHidden:YES]; // Create Download Task self.downloadTask= [self.session downloadTaskWithURL:[NSURLURLWithString:@"http://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg"]]; // Resume Download Task [self.downloadTask resume]; }
In observeValueForKeyPath:ofObject:change:context:
, we hide the cancel button ifresumeData
is nil
and we hide the resume button if downloadTask
is nil
. Build the project and run the application one more time to see the result. This is better. Right?
在 方法中,如果 resumeData 为nil,则隐藏取消按键;如果 downloadTask 为 nil,则隐藏恢复按键。编译项目并再次运行,看看效果,是否就好多了。
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if([keyPath isEqualToString:@"resumeData"]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.resumeButton setHidden:(self.resumeData==nil)]; }); }else if([keyPath isEqualToString:@"downloadTask"]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.cancelButton setHidden:(self.downloadTask==nil)]; }); } }
observeValueForKeyPath:ofObject:change:context:
is called on the main thread. It is therefore important to update the user interface in a GCD (Grand Central Dispatch) block that is invoked on the main queue.
There is one key aspect of NSURLSession
that I haven't talked about yet, session invalidation. The session keeps a strong reference to its delegate, which means that the delegate isn't released as long as the session is active. To break this reference cycle, the session needs to be invalidated. When a session is invalidated, active tasks are canceled or finished, and the delegate is sent aURLSession:didBecomeInvalidWithError:
message and the session releases its delegate.
NSURLSession 还有一个重要的内容没有提及,就是会话失效。会话对象会对其委托保持一个强引用,这就意味着只要会话处于活动状态,委托就不会被释放。为了打破这种循环引用,会话就需要被置为无效。当一个会话失效,例如活动的任务取消或者完成,委托就被发送一个URLSession:didBecomeInvalidWithError: 消息,会话就释放了委托。
There are several places that we can invalidate the session. Since the view controller downloads only one image, the session can be invalidated when the download finishes. Take a look at the updated implementation ofURLSession:downloadTask:didFinishDownloadingToURL:
. The cancel button is also hidden when the download finishes.
有几个地方我们可以使会话失效。由于视图控制器只下载一个图像,该会话可以在下载完成的时候失效。实现 URLSession:downloadTask:didFinishDownloadingToURL: 方法如下。取消按键也会在下载完成的时候被隐藏。
- (void)URLSession:(NSURLSession*)session downloadTask:(NSURLSessionDownloadTask*)downloadTask didFinishDownloadingToURL:(NSURL*)location { NSData *data = [NSData dataWithContentsOfURL:location]; dispatch_async(dispatch_get_main_queue(), ^{ [self.cancelButton setHidden:YES]; [self.progressView setHidden:YES]; [self.imageView setImage:[UIImageimageWithData:data]]; }); // Invalidate Session [session finishTasksAndInvalidate]; }
The example project we created in this tutorial is a simplified implementation of how to cancel and resume downloads. In your applications, it may be necessary to write theresumeData
object to disk for later use and it may be possible that several download tasks are running at the same time. Even though this adds complexity, the basic principles remain the same. Be sure to prevent memory leaks by always invalidating a session that you no longer need.
在本教程中创建的示例项目是实现如何取消和恢复下载任务。在你的应用中,可能又比要将 resumeData 对象写入磁盘供以后使用,或者多个下载任务同时运行它也是有可能的。尽管这些增加了复杂性,但其基本的原理是相同的。注意防止内存泄露,如果不再需要的话,要使会话对象失效。