NSURLSession のまとめ

# NSURLSession

iOS7 より導入されたバックグランドで通信できるクラス。いまさらですが、はまった点などをまとめました。

## バックグランドとは？

ここでいうバックグランドとは、単にMainThreadでないスレッドという意味ではありません。
iOS のアプリの状態の一種で、通常アプリを使用している状態はフォアグランド(Foreground)といます。この状態でホームボタンを押す等の操作を行った際は、アプリは画面の裏側へ移動してサスペンド(Suspended)状態になります。サスペンド状態ではコードを実行することはできませんが、この前段階にコードを実行できる状態があり、それがバックグランド(Background)です。

今までは、位置取得や音楽再生等限られた用途しか使用できませんでしたが、iOS7から通信処理にも利用できるようになりました。

参照：[App States and Multitasking](https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html#//apple_ref/doc/uid/TP40007072-CH4-SW3)

## NSURLSession とは

NSURLSession は今まであった NSURLConnection 等を使用した通信処理を簡略化したクラスです。NSURLConnection のような汎用性はありませんが、その代わりに処理をずっと簡素にすることができます。

参照：[NSURLSession Class Reference](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html)

### バックグランド通信

NSURLSession では、上記のバックグランド状態でHTTPを使用してファイルのアップロード/ダウンロードを行うことができます。正確には、アプリとは異なるプロセスへ移され、アプリの状態に関係なくダウンロードとアップロードの処理を継続します。このため、たとえアプリを終了されたとしても一連の通信処理が終了した段階でアプリは再起動され、メッセージを通知されます。

### HTTP 通信のみ使用可能

通信処理を簡略化してくれる NSURLSession ですが、使用できる通信プロトコルは HTTP 通信のみです。独自プロトコルはおろか、FTP等の通信は使用できません。
また、バックグランドで行えるのはファイルのダウンロード/アップロードのみになります。複雑な通信を行うことはできませんが、大容量のファイルを大量にダウンロードする際には非常に有効です。

## NSURLSession を用いたファイルのダウンロード処理

NSURLSSession の機能のうちダウンロード処理について解説します。

NSURLSession を使用した通信処理は次の流れで行います。
1. NSURLSessionConfiguration の作成
2. NSURLSession の作成
3. NSURLSessionDownloadTask の作成
4. 作成したタスクをresume する
5. ダウンロードが完了したデータを取り出す

通信を開始するに当たって必要な実装はこれだけです。後は、OS側が必要な処理を行ってくれます。

### NSURLSessionConfiguration の作成

通信時のキャッシュポリシーなどを規定するNSURLSessionConfiguration を作成します。細かく NSURLCachePolicy などを設定できます。インスタンスを作成するメソッドは三つありますが、バックグランドで通信を行うには次のメソッドを使用します。

[+ (NSURLSessionConfiguration *)backgroundSessionConfiguration:(NSString *)identifier](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/Reference/Reference.html#//apple_ref/doc/uid/TP40013440-CH1-SW2)


```objc
NSString* identifier = @"BackgroundSessionConfiguration";
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
```

引数のidentifier はセッションを識別するための識別子です。バックグランドで動作するためアプリとは異なったプロセスで通信処理が行われます。アプリが終了した後に通信処理の通知が来ることがあるため、その際にセッションのインスタンスを復元するのに使用します。この識別子はOSに登録されるようで、たとえアプリを再インストールした後でも前のセッションを取り出すことができます。

### NSURLSession の作成

作成した NSURLSessionConfiguration とデリゲートを関連付け、NSURLSession のインスタンスを作成します。デリゲート使用せずにインスタンスを作成することも可能ですが、バックグランドで動作するセッションを作成する場合は、必ずデリゲートを使用する必要があります。

[+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(id<NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013435-CH1-SW9)

```objc
NSURLSession* session =
  [NSURLSession sessionWithConfiguration:configuraion 
                                delegate:self
                           delegateQueue:nil];
```

第一引数には、作成したconfigurationを渡します。NSURLSessionConfiguraion のキャッシュポリシー等を変更する場合は、このメソッドを呼ぶ前に行う必要があります。delegate には、NSURLSessionDelegate を実装したオブジェクトを渡しますが、ダウンロードを行う場合は、NSURLSessionDownloadDelegate を実装したものを渡します。NSURLSessionDownloadDelegate はすべてのメソッドが required なので、すべてのメソッドを実装しておく必要があります。最後の delegateQueue は delegate が呼び出される NSOperationQueue を指定します。自前で作成したものを渡しても良いですが、nil を指定すると適切なものが作成されます。また、メインスレッドで呼び出したい場合は、NSOperationQueue の mainQueue を渡すと良いでしょう。

ここで渡した delegate のインスタンスは、セッションが無効化されるまでstrongで参照され続けます。このため次のいずれかのメソッドを使用して適切なタイミングで無効化してください。

* [- (void)invalidateAndCancel](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013435-CH1-SW33)
* [- (void)finishTasksAndInvalidate](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013435-CH1-SW31)

### NSURLSessionDownloadTask の作成 と resume

NSURLSession を作成したら、NSURLSessionDownloadTask を作成して実際の通信処理を登録します。作成したタスクは、resume を呼び出さない限り通信を開始しません。

```objc
NSURL* requestURL = [NSURL URLWithString:@"http://www.example.com/example.zip"];
NSURLSessionDownloadTask* task = [session downloadTaskWithURL:requestURL];
[task resume];
```

タスクを作成するには次のいずれかのメソッドを使用します。これら以外に完了時に呼び出すBlockを指定するものもありますが、バックグランドの場合はデリゲートでなければならないので使用できません。

* [- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013435-CH1-SW16)
* [- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013435-CH1-SW21)

ここで作成したタスクは、NSURLSessionConfiguration 作成時に登録した identifier にひも付けられます。NSURLSession のインスタンスが破棄されても、アプリが再インストールされても同じ識別子を使って再度 NSURLSession を作成すれば、taskを取り出すことが可能です。

待機中のタスクを取り出すには次のメソッドを使用します。

[- (void)getTasksWithCompletionHandler:(void (^)(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks))completionHandler](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013435-CH1-SW32)

```objc
[session getTasksWithCompletionHandler:^(NSArray* dataTasks, NSArray* uploadTasks, NSArray* downloadTasks){
  NSLog(@"Currently suspended tasks");
  for (NSURLSessionDownloadTask* task in downloadTasks) {
    NSLog(@"Task: %@", [task description]);
  }
}];
```

非同期で取り出されるため、取得用のBlockを引数として渡します。すでに resume されたタスクはこの一覧には載らないようです。

### データの取り出し

ダウンロードが完了するとデリゲートのメソッドが呼び出されす。

[- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDownloadDelegate_protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40013444-CH1-SW7)

ダウンロードしたファイルはlocation 引数が指し示すキャッシュ領域に保存されています。NSData 等のメソッドを使用してデータを取り出します。

```objc
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
  NSData* data = [NSData dataWithContentsOfURL:location];
  if (data.length == 0) {
    // An error occered.
  }
  ....
}
```

バックグランドの状態でダウンロードが完了して、application:handleEventsForBackgroundURLSession:completionHandler: （後述）の際に処理を行わなかった場合は、フォアグランドに戻った時にまとめて呼び出されます。またアプリを再インストールした場合、同じ識別子で NSURLSession を作成するとこのメソッドが呼ばれることがあります。その場合は、キャッシュされたファイルは削除されているので注意してください。

何らかの理由で失敗した場合は上記のメソッドは呼び出されません。成功した場合も失敗した場合も呼び出されるのは次のメソッドです。

[- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionTaskDelegate_protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40013442-CH1-SW4)

```objc
- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
  NSLog(@"Downloading has been completed.");
  if (error == nil) {
    NSLog(@"Downloading was succeeded");
    ...
  } else {
    NSLog(@"Downloading failed");
    ...
  }
  ...
}
```

## 各種デリゲートメソッド

NSURLSession を用いてバックグランドで行う際に関連するデリゲートメソッドを解説します。

### application:handleEventsForBackgroundURLSession:completionHandler: (UIApplicationDelegate)

[- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler](https://developer.apple.com/library/ios/documentation/uikit/reference/uiapplicationdelegate_protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40006786-CH3-SW49)

UIApplicationDelegate のメソッドです。アプリがバックグランドに移動した後、NSURLSession に登録したタスクが終了した際に呼び出されます。このメソッドが呼び出されると NSURLSessionDownloadDelegate が一気に呼び出されます。引数の completionHandler は完了したことをOSに通知するために使用します。非同期を前提としているので、このメソッドないで通知する必要はありません。通知を受けると snapshot (ホームキーを二回押すと出てくるやつです)　を更新して以降のデリゲートメソッドは呼び出されなくなります。

### URLSessionDidFinishEventsForBackgroundURLSession: (NSSessionDelegate)

[- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session](https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDelegate_protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40013441-CH1-DontLinkElementID_4)

バックグランドでNSURLSessionの処理が行われた場合で、application:handleEventsForBackgroundURLSession:completionHandler: が実装されてなかった場合や、completionHandler を呼んだ後に呼ばれたデリゲートがある場合は、アプリがフォアグランドに戻った後まとめて呼び出されます。バックグランドで発生していたデリゲートメソッドが呼び出された後、このメソッドが呼び出されます。