iOS7 Networking with NSURLSession: Part 2 – iPhone手機開發技術文章 iPhone軟體開發教學課程

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 的配置選項,如何取消和恢復下載任務。開始吧!

Session Configuration

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 有很大的改進瞭。

Mutability

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 對象進行配置後,這個會話配置對象就不能被修改瞭。如果你想要修改會話配置對象,那麼你必須重新創建一個新的會話。請記住,可以復制會話配置對象,然後修改會話配置對象,但是修改對會話沒有影響。

Default Configuration:默認配置

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 defaultSessionConfigurationfactory
method doesn’t change the default session configuration which it’s a copy of.

NSURLSessionConfiguration 類提供瞭三個工廠方法創建標準的配置實例對象:defaultSessionConfiguration,ephemeralSessionConfiguration,
and backgroundSessionConfiguration。第一個方法返回一個默認會話配置對象的副本,由此對應的會話行為和
NSURLConnection 對象的標準配置的對應行為相似。對通過
defaultSessionConfiguration 工廠方法獲得的默認會話配置對象進行修改並不會有效。

Ephemeral Configuration:臨時配置

A session configuration object created by invoking the ephemeralSessionConfigurationfactory
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 之前是不可能實現的。

Background Configuration:後臺配置

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: 工廠方法創建的會話配置對象,使得上傳下載數據可以在進程之外進行。上傳下載數據的任務由一個後臺守護教程管理,並且在應用程序掛起或者奔潰的時候,仍在後臺運行。我們將在本系列後面詳細介紹背景會話。

Session Configuration

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 的美妙之處在於每一個請求都是通過會話配置對象對會話進行配置。例如,為一組請求添加身份驗證的頭部就變得容易瞭。

Canceling and Resuming Downloads

In the previous tutorial, I showed you how to download an image using the NSURLSessionAPI.
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 對象包含瞭恢復下載任務必要的信息。

Step 1: Outlets and Actions

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 
 
@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

Step 2: User Interface

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。

Update the user interface.

Step 3: Refactoring

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 ()  {
    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 defaultSessionConfigurationfactory
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:@"https://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg"]];
 
    // Resume Download Task
    [self.downloadTask resume];
}

Step 4: Canceling the Download

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];
    }];
}

Step 5: Resuming the Download

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 NSDataobject,
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 恢復按鍵可以恢復下載。

Step 6: Finishing Touches

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:@"https://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)];
        });
    }
}

As George Yang points out in the comments, we don’t know whetherobserveValueForKeyPath: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.

由於不知道 observeValueForKeyPath:ofObject:change:context:方法是否會在主線程中調用,所以更新UI的內容最好還是通過 GCD 的代碼塊來實現,GCD blcok 會在主隊列中調用。

Step 7: Invalidating the Session

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];
}

Conclusion

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 對象寫入磁盤供以後使用,或者多個下載任務同時運行它也是有可能的。盡管這些增加瞭復雜性,但其基本的原理是相同的。註意防止內存泄露,如果不再需要的話,要使會話對象失效。

發佈留言