Android 8でJobSchedulerの定期実行を確認する

# 概要

本内容は、AndroidのJobSchedulerクラスをAndroid 8上で定周期で実行し、その挙動について確認した結果をまとめています。
今回は、アプリで歩数計のような機能を実現する必要があったため、その用途に適しているかどうかを検証することを目的に行いました。

# JobScheduler

[JobScheduler](https://developer.android.com/reference/android/app/job/JobScheduler?hl=ja)とは、Android 5.0 (API 21)で追加された、指定したスケジュールに従って処理をバックグラウンドで実行させる仕組みです。指定時間後に実行、指定間隔で実行といった動作が可能です（ここでは触れませんが、端末の状態や新しい画像・動画が追加されたタイミングをトリガーとして実行することもできるようです）。従来のバージョンでは、[AlarmManager](https://developer.android.com/reference/android/app/AlarmManager)などが利用されていました。

Google I/O 2018において発表された「[JetPack](https://developer.android.com/jetpack/)」において、Androidのバージョンに応じてそれらの処理を切り替える[WorkManager](https://developer.android.com/reference/androidx/work/WorkManager)が含まれているので、今後はそちらを利用していくべきかと思います。こちらを利用すると、内部的には以下のように処理が切り替わるそうです。

* API 23以降では、JobSchedulerを利用する
* API 14-22では、
    * Firebase JobDispatcherが利用できる場合はそちらを利用する
    * それ以外の場合はAlarmManagerとBroadcastReceiverを利用する

本内容はあくまでもJobSchedulerの挙動についての確認なので、WorkManagerは利用していません。（試してる時点で知らなかった、とも言いますが…。）


# Android 8以降のバックグラウンド処理

Android 8において、バックグラウンドの動作に大きな制限がかかるようになりました。これは、アプリがユーザの意図しないリソースを消費してしまうことを防ぐ目的があります。

バックグラウンド実行制限
https://developer.android.com/about/versions/oreo/background?hl=ja

このため、バックグラウンド処理を行うServiceクラスを利用するアプリは大きな影響を受けることになりました。この影響は開発時のターゲットAPIが26以上のアプリを開発する場合に影響を受けます。（Googleは開発者にターゲットAPIの下限を設けることを告知しているため、今後開発されるすべてのアプリが影響を受けることになります。）

そこで、バックグラウンドでServiceを利用していたアプリの開発者は、主に以下のような対策を迫られるようになりました。

1. ServiceをstartForegroundServiceで実行し、通知バーにアイコンを表示して実行する
2. ServiceをJobSchedulerに置換える

その他、プッシュ通知や特定の利用など一部の例外はありますが、基本的にバックグラウンド動作はあまり開発者の好き勝手にできなくなりました。


# JobSchedulerの定周期動作

今回、定周期で特定の処理を行うために、JobSchedulerが使えないかどうかを検討することとしました。JobSchedulerの主な動作仕様は次のようになります。

* 定期実行する場合、ジョブID毎に実行間隔を指定でき、最小間隔は約15分。それより小さい時間間隔を指定しても15分に設定される。
* 実行間隔は前後し、正確な周期は保証されない。
* 最大実行時間は約10分間。(5.1.1までは1分間だった模様)
* [DOZE](https://developer.android.com/training/monitoring-device-state/doze-standby?hl=ja)状態では動作しなくなり、一定のタイミングで訪れるメンテナンスウィンドウでまとめて実行される。
* 同時に実行可能なジョブの数には上限があり、環境によって異なる。



# 実際に動かしてみる

実際の確認するため、簡単なテスト用アプリを作ってみました。定周期で歩数を取得して、それを出力しています。
その際、JobSchedulerに設定するJobInfoを以下のように設定しました。実行間隔に0を指定していますが、15分より小さい値なので内部で15分に設定されているはずです。setPersistedとsetRequiredNetworkTypeは今回あまり関係無いのでパスします。

~~~
JobInfo info = new JobInfo
        .Builder(0) // JobID=0の指定
        .setPersisted(true) // 端末再起動後も実行する
        .setPeriodic(0, JobInfo.getMinFlexMillis()) // 実行間隔=0と実行遅延許容時間の指定
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE) // ネットワーク状態と無関係に実行
        .build();
scheduler.schedule(info);
~~~

その出力結果は以下になります。18:58:01に開始し、00:15:10のデータまで測定し続けています。

<img width="540" alt="test1.png" src="https://qiita-image-store.s3.amazonaws.com/0/78503/e665bfc0-4fe0-52bb-dd25-234503453ff4.png">

ざっと見ると、概ね15分間隔で実行されていますが、あまり正確でないことが分かります。しばらくすると5分ほどずれが発生してバラツキが生じているのは、端末が浅いDoze状態に突入しているものと推測されます。

次に、もう少し時間を進ませた時のデータを見ます。

<img width="540" alt="test2.png" src="https://qiita-image-store.s3.amazonaws.com/0/78503/8ebb77a4-7640-a6e7-7ab8-d42c8c90fada.png">

ここでは、3時を少し過ぎて、朝の8時過ぎ辺りまで携帯の操作を完全に停止しています。4時～7時辺りで、急激に実行間隔が長くなっていることが分かります。これは、端末が深いDoze状態に突入しているものと考えられます。

当初自分は、実行間隔が長くなっても。メンテナンスウィンドウのタイミングで周期分の処理が一度に実行される（つまり、1時間停止していたら4回分実行される）ものと思っていたのですが、単純に実行間隔が長くなっているだけのようです。


# 複数のJobSchedulerで定期実行

ここで少し実験してみました。実行間隔はJobInfoに設定するJobID毎に指定するもので、タイミングをずらして異なる処理を複数走らせると、もっと短い間隔の定期実行処理を擬似的に再現できるのではないかと。

とりあえず、1分周期で実行される処理を実現させることを考えてみました。
そのために、ここでは2種類のJobSchedulerを作成します。一つ目のJobSchedulerは指定時間後に処理を実行させるもの、二つ目のJobSchedulerは定期的に処理を実行させるものです。一つ目のJobSchedulerで、二つ目のJobSchedulerを叩きます。15分の間に1分間隔で実行するため、2つ目のJobSchedulerは15個のインスタンスが必要になります。

一つ目のJobSchedulerのJobInfo

~~~
// 15個のJobSchedulerを実行
for (int i = 1001; i <= 1015; i++) {
    JobInfo info1 = new JobInfo
            .Builder(i, componentName) // JobIDが被らないように、1001〜１０１５を設定
            .setPersisted(true)
            .setMinimumLatency((i - 1001) * (1000 * 60)) // 1〜15分後に実行
           .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE)
           .build();
    scheduler.schedule(info);
}
~~~

二つ目のJobSchedulerのJobInfo

~~~
JobInfo info2 = new JobInfo
        .Builder(jobParameters.getJobId() - 1000, componentName) // JobIDに１〜15を設定
        .setPersisted(true)
        .setPeriodic(0, JobInfo.getMinFlexMillis())
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE)
        .build();
scheduler.schedule(info);
~~~

その結果、以下のようになりました。今回はGPS情報を一緒に出力させていたのですが、そこはプライバシー上の理由により黒塗りにしています。

<img width="434" alt="test3.png" src="https://qiita-image-store.s3.amazonaws.com/0/78503/73ebaaa0-7823-62ee-05a6-ead813ef8d85.png">

概ね1分周期で動いていますが、やはりあまり正確ではありません。
しばらくすると、次のような動きになりました。

<img width="434" alt="test4.png" src="https://qiita-image-store.s3.amazonaws.com/0/78503/c3113f4e-69fb-44f8-fe06-81d1e8384406.png">

02:34:35～の辺りで実行順序（IDの順序）が狂っています。実行時間を見ると、短時間に一度に実行されているので、Doze状態からメンテナンスウィンドウのタイミングで一度に実行されているものと推測されます。その後、元の1分間隔の実行サイクルに戻っているようですが…。

結論としては、JobSchedulerの実行サイクルはブレがあり信用できるようなものではない上、Doze状態で動きが変わって結果が読めなくなるので、こういうトリッキーな実装は止めた方がいいと思います。


# まとめ

今回の確認で、JobSchedulerの特性がある程度分かりました。JobSchedulerはバックグラウンドで定期的な処理を行いたい場合に利用できますが、その実行間隔や実行タイミングは信用してはいけないようです。

そのため、目的の用途でJobSchedulerを採用するには不向きであると判断し、採用を見送りました。ただ、こうして実験したことでメリット・デメリットが見えてきたので、今後の採用判断に役立ちそうです。


# 主な参考資料

* Jobschedulerの基本
  https://qiita.com/naoi/items/f1d00a79196d3d2d3a81

* Android M/NのDozeによる制限とバックグラウンドタスク実行に関するまとめ
  https://qiita.com/FumihikoSHIROYAMA/items/b1d6dbda120462d0e209