post Image
iOSのBackground Transferがよくわからない人用に整理した

はじめに

アプリで大きなサイズの動画や画像をアップロードしようとする時、ユーザを待たせないためにアプリが動作していないときでもアップロードできるようにしたいという要望が必ず上がる。そういうとき、NSURLSessionによるBackground Transferを使ってアプリが動作していないときでもコンテンツをアップロードする方法について行き着くと思うが、何から調べたら良いのかということや、iOS4からあるバックグラウンド要求について、Background fetchなどと紛らわしいという事があったのでそれを整理しておく。結局、実装は出来て動くもののリファレンスを読んだり実行してもよくわからないとこもあるので知ってる人はコメントに書き込んでもらえると助かります。

言いたいことを3行にすると

  • バックグラウンドの定義を明らかにしておく
  • AFNetworking, Alamofireなどの通信ライブラリは使わずAppleのサンプルコードを動かす
  • アップロードに必要なdelegateがある

バックグラウンドの定義を明らかにしておく

アプリが動作していないときとは何かについて定義ちゃんとしておくべきだと思う。

公式のリファレンスによるとアプリケーションの状態は次のようになっている。

いちばん重要なのは、NSURLSessionのBackground Transferを使うと言っても、状態が図の Background状態で通信を行い続けるわけではない。Not running状態やInactive状態, Suspended の状態でも通信が動作すると思われる。なぜ「思われる」と書くのかというとInactiveの状態は一瞬だし、Suspended状態やNot Running状態に遷移する際にイベントが発生してくれるわけではないのでアプリ側では知ることができないからだ。

つまり、Background Transferのバックグラウンドとは図にあるような厳密なアプリの状態を表しているのではなくアプリがフォアグラウンドで利用していないということを意味している。言い方を変えると、Background Transferは Foreground状態以外で動作するのであってそこは意図しないようになっている(把握して理解したいんだけどな)。

この文章では

  • 先述の図にあるBackgroundを示す場合は Background状態 と記載
  • 単純にフォアグラウンドでない場合を示す場合はバックグラウンドもしくはBackgroundとして記載する

もうこの時点でややこしい。Background Transferという用語は利用者視点での用語に思える。開発者間で「バックグラウンドで送信するようにして…」という言葉が出ると先述の図のようなものをイメージするだろうが厳密にはそうではない。繰り返しになるが、おそらくNot running状態やInactive状態, Suspended状態でも通信する。

しかし、どうやってフォアグラウンド以外で送信しているのかというと、iOSにより別プロセスが立ち上げられそのプロセスに通信処理を任せている。アプリとは別プロセスでの通信のために公式のリファレンスによると制限を満たす必要があるようだ

  • delegateを実装しなければいけない
    • おそらくNSURLSessionのdelegateを使うことを意図している
      • フォアグラウンド状態でも利用できるdelegateがある(クロージャは使えない)
    • AppDelegateで利用するdelegateもあり、アプリが別プロセスから呼び出された際に動作する
      • application:handleEventsForBackgroundURLSession:completionHandler:
  • HTTP/HTTPSのみ
  • リダイレクトは自動で追跡される
  • ファイルアップロードのみ
    • 例えばPOSTでアップロードする場合bodyをファイル化し、そのパスを通信時に渡す
      • マルチパートでuser=1のようなパラメータもコンテンツとまとめてファイル化する
  • NSURLSessionConfigurationのisDiscretionaryがtrue

似ている技術

バックグラウンド実行は似てる技術があるのでそれを整理する。大雑把に知っておくだけでも使い分けのためにあらかじめ考察することが出来る。

https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html#//apple_ref/doc/uid/TP40007072-CH4-SW1

  • Executing Finite-Length Tasks(iOS4から)
    • ホームボタンを押してアプリを切り替えてもiOS6までは10分間、iOS7からは最長3分間実行できる
      • 3分に短くなったのは他のBackground実行を用途に応じて使ったほうがバッテリーに負荷をかけないからだと納得しよう
    • アプリが Background状態に入る際に処理を継続することをiOS側に要求する方向で実装する
    • 1分くらい延長したいとか、あらかじめ時間が決まっている場合に使える
    • 考えられる用途
    • フォアグラウンドで終わらない処理をできるだけ続けたい場合に使える
  • Background Fetch(iOS7から)
    • アプリがバックグラウンドの際にiOS側から特定のdelegateを呼び出す
    • application:performFetchWithCompletionHandler:を実装する
    • 考えられる用途
      • 最新ニュース記事を決まったエンドポイントから通信して取得する
      • 電子書籍のページを同期するために通信する

ここまでのまとめ

技術用語なんだから「Background Transfer」というアプリ利用者目線での用語でなく「アプリとは別のプロセスを使った通信」にしてくれよ

AFNetworking, Alamofireなどの通信ライブラリは使わず Appleのサンプルコードを読む

自分の場合、とりあえずライブラリを使えば良きに計らってくれるだろうと思ったが駄目だった。ドキュメントは薄いしコードを読んでも結局「Background Transfer」とは何かがわからないと実装ミスがわからない。急がば回れでNSURLSessionのdelegateを実装していくほうがいい。

Appleのサンプルコード

https://developer.apple.com/library/content/samplecode/SimpleBackgroundTransfer/Introduction/Intro.html

公式のサンプルコードはダウンロードしかしてないがこれでやり方の80%は分かる。
古いサンプルコードなのでBackground FetchをXcodeからonにする必要などがあるかもしれない。

分かりづらかった点を書いておく

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier

  completionHandler:(void (^)())completionHandler

{

    BLog();

    /*

     Store the completion handler. The completion handler is invoked by the view controller's checkForAllDownloadsHavingCompleted method (if all the download tasks have been completed).

     */

    self.backgroundSessionCompletionHandler = completionHandler;

}

まずAppDelegateで実装したdelegateについて、

  • このdelegagteは別プロセスが動作していない状態のアプリを呼び出す際に動作する
  • backgroundSessionCompletionHandlerはAppDelegateが保持している
    • これはタスクが完了してUIを更新した際に呼び出すcompletionHandlerだと思えばいい
    • このcompletionHandlerを呼び出すとiOSはアプリのスクリーンショットを撮ってアプリのリストを更新する
    • なぜこんなことをすると思う?
      • iPhoneのホームボタンを押すとアプリ一覧のリストがスクリーンショット付きで表示されている
      • このスクリーンショットを更新しないと、一覧から選んだ後に画面が一瞬で処理後に変わってしまう
      • このスクリーンショットを更新していると、一覧からすでに変更後の画面になっているというわけ
    • 具体例を示すと、アップロード予定の表示をアップロード済みの表示に変えるなどをした後にcompletionHandlerを呼び出す

アップロードに必要なdelegate

最低限必要なDelegateを書いておく。HogeUploadAPIクラスにextensionでdelegateを実装する想定で

extension HogeUploadAPI: NSURLSessionDataDelegate {

    // swift3版
    /// 一つの通信結果に対して都度都度呼び出されることを想定する
    /// didReceive dataは繋げて一つのDataにする
    func urlSession(_ session: URLSession,
                    dataTask: URLSessionDataTask,
                    didReceive data: Data) {
        // swift3ではvar responseData: Dataにappendすればいい
        responseData.append(data)
    }
}

進捗時に呼び出されるメソッドも有る。これがなくてもいいが、アプリ動作中に通信途中の進捗表示したい場合や、もしくはキャンセルしたい場合にNSURLSessionTaskからキャンセル処理ができるため、次のようにしておくと良いかも。

extension HogeUploadAPI: NSURLSessionTaskDelegate {

    /// アップロード進捗時に呼び出される
    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didSendBodyData bytesSent: Int64,
                    totalBytesSent: Int64,
                    totalBytesExpectedToSend: Int64) {

        progressHandler(task)
    }
}

次に通信完了のdelegate。フォアグラウンドでもそうでなくてもとにかく通信完了時に呼び出されるが、通信キャンセルされても呼び出される。

通信のキャンセルというのはユーザが明示的に行う以外に、「アプリがバックグラウンドにありタスクを削除した場合」にもキャンセルが行われる。ユーザが利用を明示的にやめたとみなされて別プロセスで通信が起こっている場合にそれをやめることが出来るという意図だろう。

    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {

        guard backgroundUploadTaskID == task.taskIdentifier else {
            // 何もしたくないのでcompletionを呼ばない
           return
        }

        // 終了処理的な何か
    }

backgroundUploadTaskIDを保持し、完了時に比較することで明示的に実行した通信でない場合は何もしないようにしている。これは自分の作っているアプリの前提状況として「アップロードは常に一つずつ」という制限があったためこのような作りで済んでいる。

別プロセスでの通信を行う際にキャンセルされる流れは次のようになる

  • フォアグラウンドで通信開始
  • アプリをバックグラウンドにする(ホームボタンを度押して3分くらい放置)
    • 通信が別プロセスに渡される
  • アプリのリストからタスクを削除(ホームボタン2度押しでリストからシュッと消す)
    • 別プロセスが停止し通信がキャンセルされる
  • アプリを起動する
  • 通信を開始する

つまり、ここでやりたいのは別プロセスでの通信がキャンセルされた場合に何もしたくないが、別プロセスかどうかを正しく判定することが難しかったため、アプリで通信した場合にはtaskIDが保持されているもの、という前提を利用している。

その他

参考のリンクを貼っておきますが、自分は読んでも理解できんかった。しかしどうして理解できないかもあると他の人が読んで挫折しないと思うので残しておきます

アプリが寝てる間に…: Background Transfer Services

https://realm.io/jp/news/gwendolyn-weston-ios-background-networking/

  • handleEventsForBackgroundURLSessionでセッションを生き返られせる
    • handleEventsForBackgroundURLSessionが動作するのは完了時なのに再度セッションを利用する意図が分からない
    • やるならこのdelegateじゃないんじゃない?

URLSessionDownloadTask でちょっと大きめなデータでも寝ている間にダウンロード

http://qiita.com/codelynx/items/f4ff935058addaa9c5d7

  • taskDescriptionにJSONを渡しておいて通信結果として呼ばれた際に確認する
    • おそらく複数のセッションを利用して複数の種類のダウンロードを行う場合の管理としては有効そう
    • 自分の場合と違いすぎてその前提がないと分かりづらい

iOSで無限バックグラウンドアップロード(に挑戦してみた話)

https://speakerdeck.com/ainame/iosdewu-xian-batukuguraundoatupurodo-nitiao-zhan-sitemitahua

よくわからんのはリファレンスにあるisDiscretionaryについて

  • large amounts
    • 量が多い。つまりファイルが1個2個…の数が多い(逆にlarge amountだったらサイズがでかそう)
  • large files
    • 意図して書き分けてるなら量が多いよりfilesのサイズが大きいような気がする(それとも書き分けただけ?)

『 Swift 』Article List