📑

iOS13から追加されたBackground Tasksの挙動を調査した

2021/03/25に公開

目的

iOS13から追加されたBackground Tasksは、今までiOSでは難しいとされていたバックグラウンドでの処理実行のための仕組みです。これまでもバックグラウンド処理のための仕組みは用意されていましたが、30秒程度しか処理を実行できないなど何かと制約の多いものでした。

今回調査するBackground Tasksはその制約が大きく緩まって、機械学習のトレーニング処理のような重めの処理を実行できると謳われています。たまたまBackground Tasksが有効活用できそうな機会があったため、その挙動を調査することにしました。

Background Tasksには大きく分けてBGAppRefreshTaskBGProcessingTaskの2種類のタスクが存在しますが、今回調査するのはBGProcessingTaskです。

環境

  • 実機 iPad Pro (11-inch) (2nd generation), iOS 14.4.1
  • Xcode 12.4

調査方法

1分ごとにログを投げるタスクを作成します。ログにはタスクIDを省略したものとその実行中における送信回数を付与します。
設定に違いのある4種類のタスクを用意し、それぞれを同時にスケジュールし、iPadを日常利用してどのようなログが送信されているかを調べます。
今回ログ基盤はGoogleAppsScriptでエンドポイントを用意し、Spreadsheetに記録するという簡易なものにしました。

コードの一部を抜粋。

    static func handleTask1(task: BGProcessingTask) {
        // 自己再スケジュール部分
        let req = BGProcessingTaskRequest(identifier: task.identifier)
        req.requiresExternalPower = true
        try! BGTaskScheduler.shared.submit(req)

        task.expirationHandler = {
            send(value: "expire", task: task)
        }

        for i in 0... {
            if i % 60 == 0 {
	        send(value: "\(i / 60)", task: task)
            }
            sleep(1)
        }
    }
    
    private static func send(value: String, task: BGTask) {
        let id = task.identifier.replacingOccurrences(of: "com.example.myapp.", with: "") // タスクIDの省略部分
        let url = URL(string: "https://script.google.com/macros/s/xxxxxxxxxx/exec?v=2&id=\(id)&value=\(value)")!
        URLSession.shared.dataTask(with: url).resume()
    }

タスクの設定は以下の表のようにした。
自己再スケジュールの有無とは、上記ソースコード中コメントにある部分で、タスク処理内に自身と同じタスクを再度スケジュールする処理を入れるかどうか、という意味です。
今回処理は無限に続くというものであるためsetTaskCompleted(success:)を呼ぶタイミングはありません。

ID 自己再スケジュール requiresExternalPower
com.example.myapp.testing1 有り false
com.example.myapp.testing2 有り true
com.example.myapp.testing3 無し false
com.example.myapp.testing4 無し true

実行結果

実際のログをcsvにしてgistに記載しました。また適宜iPadの状態が変化した部分や気づいたことを添えています。

https://gist.github.com/sidepelican/bcad389c73fc6b99051254236e5ca2ac

ちなみにコメント中の「フォアグラウンド」はiPadの画面がついている状態、「バックグラウンド」は画面がついてない状態を指しています。音楽を再生していても画面がついていなければ「バックグラウンド」です。各種アプリの起動状態は関係ありません。

気づいたこと

  • タスクは30分につき1回、1回あたり5分実行される
  • 自己再スケジュールをしなかった場合、タスクが完了していなくても5分経過したら以降実行されなくなる
  • 1つのアプリは同時に10個までのタスクをスケジュールできる(とドキュメントに記載されていた)
  • タスクは同時に3つまで並列に走る
    • 他のアプリもタスクをスケジュールしていた場合にどうなるかは未調査
  • タスク実行中に端末がフォアグラウンドになったときはタスクが中断される
    • この際、ローカルの関数スコープレベルで状態が保持される。つまりメモリに乗りっぱなし
  • 中断されていたタスクがあって同じタスクがスケジュールされていた場合はそれぞれが起動する。つまり同じタスクIDでも同時に2つ実行されている状態があり得る
  • 中断されていたタスクが復帰する際は5分間のカウントがリセットされる。つまりトータルで5分以上実行されるケースがありうる
  • 端末がバックグラウンドに入ると調子がいいときは1分以内に開始される
  • 並列にタスクが走っていると通信処理が失敗しやすい?
    • 並列にタスクが走っているときにログの欠損が見られるため。URLSessionの使い方やログ基盤自体が全体的に雑なので別原因かもしれない。
  • 非充電中には実行されない?
    • 非充電中はログが一度も流れてきていない。requiresExternalPowerの設定に関係なく。
    • タスクの実行はいろいろな要素をみてOSが判断するため、今回たまたまそうなったというだけかもしれない
  • expirationHandlerの中でネットワーク処理は実行できない?
    • expirationHandlerに設定したexpireのログが流れてきていないため

考察

今回の計測は比較的雑でしたが、それでもいろいろな性質を知ることができました。
充電されている状態でさえあれば30分ごとに5分計算できるため、仮に1時間かかる処理があったとしても深夜に6時間放置されていれば完了できそうですね。
また同じアプリからでもタスクIDごとにそれぞれ独立して実行されるので、タスクを並列化できればさらに計算時間を得られそうです。
とはいえ今回の検証はCPU負荷的にはほぼゼロなタスクなので実際にCPU負荷のかかるタスクを実行させた場合はまた挙動が変わるかもしれません。

ログが送信される様子を観察するのは畑の成長の様子を見守るようで楽しかったです。

Discussion