iOS7バックグラウンド通信(NSURLSession)のライフサイクルまとめ


iOS7でBackground Transferを使うときのライフサイクルが分かりにくいので実際に試してみました。アプリの生死に関わらず通信が継続されるというのは本当なのか？通信成功の処理と通信失敗の処理はどこに実装すればいいのか？をはっきりさせます。

iPhone5実機(iOS7.1.2)にて以下のデモアプリを使用してコールバックを確認しました。このデモアプリは複数ファイルダウンロード時の挙動が分かりやすくて良いです。

>[Background Transfer Service in iOS 7 SDK: How To Download File in Background](http://www.appcoda.com/background-transfer-service-ios7/)


## アプリフォアグラウンドでダウンロード(1セッション2タスク)したとき

複数のファイルをダウンロードしているとき、アプリがフォアグラウンドであればリアルタイムで次のメソッドが実行されます。
ファイルのダウンロード順は保証されません。

* (ファイル1)ダウンロード進捗時 `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:` (NSURLSessionDownloadDelegate)
* (ファイル1)ダウンロード完了時 `URLSession:downloadTask:didFinishDownloadingToURL:` (NSURLSessionDownloadDelegate)
* (ファイル1)ダウンロード完了後 `URLSession:task:didCompleteWithError:` (NSURLSessionTaskDelegate)
* (ファイル2)ダウンロード進捗時 `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:`  (NSURLSessionDownloadDelegate)
* (ファイル2)ダウンロード完了時 `URLSession:downloadTask:didFinishDownloadingToURL:` (NSURLSessionDownloadDelegate)
* (ファイル2)ダウンロード完了後 `URLSession:task:didCompleteWithError:` (NSURLSessionTaskDelegate)

全タスクが終了したときのコールバックは無いようです。
すべてのタスクが完了し、セッションが不要になったら`finishTasksAndInvalidate` (NSURLSession)を実行してセッションを破棄します。

## アプリバックグラウンドまたはプロセスが死んでいる状態でダウンロード(1セッション2タスク)したとき

アプリがクラッシュしたとき、OSの判断でアプリプロセスが殺されたとき、アプリがバックグラウンドにあるときは同じライフサイクルとなります。
ファイルのダウンロード順は保証されず、すべてのファイルダウンロードが完了したときに以下のコールバックが順番に、まとめて実行されます。また、進捗コールバックも実行されません。

* バックグラウンドでセッションが完了した `application:handleEventsForBackgroundURLSession:completionHandler:` (UIApplicationDelegate)
* (ファイル1)ダウンロード完了 `URLSession:downloadTask:didFinishDownloadingToURL:` (NSURLSessionDownloadDelegate)
* (ファイル1)ダウンロード完了 `URLSession:task:didCompleteWithError:` (NSURLSessionTaskDelegate)
* (ファイル2)ダウンロード完了 `URLSession:downloadTask:didFinishDownloadingToURL:` (NSURLSessionDownloadDelegate)
* (ファイル2)ダウンロード完了 `URLSession:task:didCompleteWithError:` (NSURLSessionTaskDelegate)
* `URLSessionDidFinishEventsForBackgroundURLSession:` (NSURLSessionDelegate)

バックグラウンドダウンロードのライフサイクルでは`handleEventsForBackgroundURLSession`から`URLSessionDidFinishEventsForBackgroundURLSession`までを30秒以内に完了する必要があります。`handleEventsForBackgroundURLSession`で受け取った`completionHandler`を`URLSessionDidFinishEventsForBackgroundURLSession`で実行します。30秒経過しても`completionHandler`が呼ばれていなければ、プロセスは強制終了されます。

この一連の流れはセッションごとに実行されるはずなので、複数のセッションを同時に実行している場合には`completionHandler`をセッションごとに保持できるように配慮が必要です。

すべてのタスクが完了し、セッションが不要になったら`finishTasksAndInvalidate` (NSURLSession)でセッションを破棄します。

## アプリバックグラウンドでダウンロード実行中にユーザの操作でアプリが終了したとき

ユーザがアプリ切り替えからスワイプ操作でアプリを終了すると、バックグラウンドダウンロードも強制的に切断され、エラー扱いとなります。
`NSURLSession`のコールバックも一切呼ばれることなく終了してしまいますが、ユーザ操作によるアプリを再起動時にエラーを検出することは可能です。

* アプリがバックグラウンドかつダウンロードが進行中の状態で、ユーザ操作によってアプリが終了した
 * -> コールバックは一切実行されない。ダウンロードキャンセルされる。
* ユーザ操作によってアプリが再起動した
 * -> `URLSession:task:didCompleteWithError:` (NSURLSessionTaskDelegate) がエラー付きで実行される。
 * エラー内容は「The operation couldn’t be completed. (NSURLErrorDomain error -999.)」
(エラーコード-999は`NSURLErrorCancelled`)
 * セッションIDからセッションを復元することで初めてエラーハンドリングが可能

この場合、OSからセッションIDを受け取ることはできないため、アプリ側でセッションIDを永続化しておく必要があります。

## まとめ

フォアグラウンドダウンロード、バックグラウンドダウンロード、エラー発生時のすべての場合で`didCompleteWithError`は実行されています。`didCompleteWithError`に成功時と失敗時のロジックを実装しておくのが一番素直だと思います。

また、セッションのinvalidate処理も`didCompleteWithError`に実装することができれば安全です。

ということで、以下の結論となりました。

* アプリの生死に関わらず通信が継続されるというのは本当なのか？
 * -> ユーザ操作によるアプリ終了時のみ、通信は中断される。それ以外は基本的に通信継続。ただし、アプリバックグラウンド状態でダウンロード進捗を受け取ることはできない。
* 通信成功の処理と通信失敗の処理はどこに実装すればいいのか？
 * -> とりあえず`didCompleteWithError`で良い。セッションの復元やセッションのinvalidate処理も忘れずに。

### 参考

* [Life Cycle of a URL Session - Apple Developer](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/NSURLSessionConcepts/NSURLSessionConcepts.html)
* [Using NSURLSession - Apple Developer](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html)
 * セッションIDからのセッション復元や`completionHandler`の扱いについて、サンプルコードが掲載されています