iOS13から追加されたBackground Tasksの挙動を調査した
目的
iOS13から追加されたBackground Tasksは、今までiOSでは難しいとされていたバックグラウンドでの処理実行のための仕組みです。これまでもバックグラウンド処理のための仕組みは用意されていましたが、30秒程度しか処理を実行できないなど何かと制約の多いものでした。
今回調査するBackground Tasksはその制約が大きく緩まって、機械学習のトレーニング処理のような重めの処理を実行できると謳われています。たまたまBackground Tasksが有効活用できそうな機会があったため、その挙動を調査することにしました。
Background Tasksには大きく分けてBGAppRefreshTask
とBGProcessingTask
の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の状態が変化した部分や気づいたことを添えています。
ちなみにコメント中の「フォアグラウンド」はiPadの画面がついている状態、「バックグラウンド」は画面がついてない状態を指しています。音楽を再生していても画面がついていなければ「バックグラウンド」です。各種アプリの起動状態は関係ありません。
気づいたこと
- タスクは30分につき1回、1回あたり5分実行される
- 自己再スケジュールをしなかった場合、タスクが完了していなくても5分経過したら以降実行されなくなる
- 1つのアプリは同時に10個までのタスクをスケジュールできる(とドキュメントに記載されていた)
- タスクは同時に3つまで並列に走る
- 他のアプリもタスクをスケジュールしていた場合にどうなるかは未調査
- タスク実行中に端末がフォアグラウンドになったときはタスクが中断される
- この際、ローカルの関数スコープレベルで状態が保持される。つまりメモリに乗りっぱなし
- 中断されていたタスクがあって同じタスクがスケジュールされていた場合はそれぞれが起動する。つまり同じタスクIDでも同時に2つ実行されている状態があり得る
- 中断されていたタスクが復帰する際は5分間のカウントがリセットされる。つまりトータルで5分以上実行されるケースがありうる
- 端末がバックグラウンドに入ると調子がいいときは1分以内に開始される
- 並列にタスクが走っていると通信処理が失敗しやすい?
- 並列にタスクが走っているときにログの欠損が見られるため。URLSessionの使い方やログ基盤自体が全体的に雑なので別原因かもしれない。
- 非充電中には実行されない?
- 非充電中はログが一度も流れてきていない。requiresExternalPowerの設定に関係なく。
- タスクの実行はいろいろな要素をみてOSが判断するため、今回たまたまそうなったというだけかもしれない
- expirationHandlerの中でネットワーク処理は実行できない?
- expirationHandlerに設定した
expire
のログが流れてきていないため
- expirationHandlerに設定した
考察
今回の計測は比較的雑でしたが、それでもいろいろな性質を知ることができました。
充電されている状態でさえあれば30分ごとに5分計算できるため、仮に1時間かかる処理があったとしても深夜に6時間放置されていれば完了できそうですね。
また同じアプリからでもタスクIDごとにそれぞれ独立して実行されるので、タスクを並列化できればさらに計算時間を得られそうです。
とはいえ今回の検証はCPU負荷的にはほぼゼロなタスクなので実際にCPU負荷のかかるタスクを実行させた場合はまた挙動が変わるかもしれません。
ログが送信される様子を観察するのは畑の成長の様子を見守るようで楽しかったです。
Discussion