iOS7でBackground Transferを使うときのライフサイクルが分かりにくいので実際に試してみました。アプリの生死に関わらず通信が継続されるというのは本当なのか?通信成功の処理と通信失敗の処理はどこに実装すればいいのか?をはっきりさせます。
iPhone5実機(iOS7.1.2)にて以下のデモアプリを使用してコールバックを確認しました。このデモアプリは複数ファイルダウンロード時の挙動が分かりやすくて良いです。
Background Transfer Service in iOS 7 SDK: How To Download File in Background
アプリフォアグラウンドでダウンロード(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
- Using NSURLSession - Apple Developer
- セッションIDからのセッション復元や
completionHandler
の扱いについて、サンプルコードが掲載されています