WorkManagerの内部を見てみた

f:id:qurangumio:20210907153258p:plain

はじめに

皆さん、こんにちは!mikanでAndroidエンジニアをしているgumioです。

twitter.com

今回はmikan androidにWorkManagerを導入しようとした際に色々面白かったので、それを紹介しようかなと思います。

こちらの記事は以下のような流れになっています。

  1. WorkManagerを知らない人向けに簡単な概要とメリット
  2. WorkManagerのユースケース
  3. WorkManagerの内部挙動
  4. 検証してtoolなどを使って覗いてみた
  5. さいごに

WorkManagerを知らない人向けに簡単な概要とメリット

Android Jetpackの仲間です。

developer.android.com

あるバックグラウンドタスクを実行するScheduler APIです。

今までバックグラウンドタスクに利用されていたFirebaseJobDispatcherGcmNetworkManagerJobSchedulerなどがあったが、WorkManagerを利用すれば以下の図のように内部で分岐してくれるので、API levelとかを意識する必要がないメリットがある + 電池寿命も考慮されており、電池寿命が伸びるメリットもあると公式は言うております。

f:id:qurangumio:20210907150508p:plain
WorkManagerの内部分岐について

ちなみにWorkManagerはAPI level 14から機能するので、安心してください。

WorkManagerの全体的なメリットとしては

  • API level 14から気にせずに機能する
  • シンプルで一貫性がある
  • 信頼性の高い非同期タスク
  • 電池寿命がよくなる
  • RxとCoroutineのサポートもされている

今回WorkManagerの詳細な使い方などは日本語ドキュメントが充実しているため、紹介しないです。

WorkManagerのユースケース

Coroutineとはどこで使い分けたらいいの?どういう時にWorkManagerを使うべき?とか少なからず思いますよね。

androidドキュメントににバックグラウンドタスクのタスクのガイドが用意されてますので、これに従うでいいと思います。

developer.android.com

僕なりの解釈としてはこんな感じだと思いましたが、どうでしょう?

  • 短時間 or 長時間かかって、裏側で必ず動かしてほしいタスク
    • 厳密には書かれていませんが、10 分以上は実行できるらしい
  • 電源が急に落ちる、いきなりプロセスキルされても必ず実行されたい 延期させたい: ネットワークがオフラインになっている、充電が充分にない場合に延期させて、条件を充分に満たした場合に実行したい

公式でも以下のように述べられてます

WorkManager は、ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業を対象としています。次に例を示しますログやアナリティクスをバックエンド サービスに送信するアプリデータをサーバーと定期的に同期する

mikanでもログを送るときに使用しようとしています。

アプリデータをサーバーと定期的に同期するはfetchしてくるイメージだと思います。アプリのデータをpushすることもできなくはないですが、WorkManagerが入力を受け付けれるをデータ量は10KBまでとなっているので、あまり何かを載せるのは期待できません。

それでは、次に実際の挙動を見ていきましょう!

WorkManagerの内部挙動

さて、今回一番紹介したい部分で、WorkManagerの謳い文句の

「ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業」

これって、結局どうやってるんだろう??ってなりました。 そこで内部の動きを簡易ではありますが、追ってみます。

まず、簡単なWorkManagerの流れのおさらいですが

  1. 行いたいタスク(Worker)を定義する
  2. このタスクの実行方法とタイミングを決めるRequestを作って依頼する
  3. WorkManagerを通じて、システムに2.のRequestを飛ばす(enqueue)

すごくシンプルですね。

このとき、WorkManagerのStateは以下の図のようになっていて、一度きりのRequest(OneTimeWorkRequest)と定期的なRequest(PeriodicWorkRequest)で少し異なりますが、終わりがあるかないかの違いだけで基本は同じです。

以下、処理の状態  |  Android デベロッパー  |  Android Developers より

f:id:qurangumio:20210907150506p:plain
一度きりのRequestのとき

f:id:qurangumio:20210907150503p:plain
定期的なRequestのとき

それでは、enqueueされたあとのRequestたちはどのように扱われてるのでしょうか?

WorkManager basics. Getting started with WorkManager | by Lyla Fujiwara | Android Developers | Medium より

f:id:qurangumio:20210907150513p:plain
enqueueされたあとの流れ

  • Internal TaskExecutorが、requestされたものをWorkManager DB(Room)にWorkRequest情報を保存します
    • Internal TaskExecutorはsingle threadのExecutorで、実際にenqueueされたrequestをさばく役割を持っています
  • WorkRequestのConstraintが満たされると(すぐにでも可能ですが)、Internal TaskExecutorはWorkerFactoryにWorkerを作成するように指示します
  • その後、defaultのExecutorは、mainThreadからWorkerのdoWork()メソッドを呼び出します

developer.android.com

なるほど!RequestされたものがDBに保持されていたんですね。このDBにはRoomが使われてました!! Requestされたものは制約が満たされるまではDBでおとなしくしてるんですね。この制約は端折っていましたが、Requestを作るときに指定することができます。

例えば、ネットワークがオンラインのときかつ、充電中のときのみ実行される制約が設定されます。

val constraints = Constraints.Builder()
  .setRequiredNetworkType(NetworkType.CONNECTED)
  .setRequiresCharging(true)
  .build()

先程のStateでRUNNINGになってたとしても処理が完了する前に電源落とされたり、オフラインになったりするとENQUEUEEDに戻り、またDBに大人しく返ってたんですね。

改めて、最初の謳い文句を再度見たときに、なるほどとなるのではないでしょうか?

「ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業」

検証してtoolなどを使って覗いてみた

最後に少しだけ挙動を確認してみて、終わりにしようと思います! 以下のような1...10を出力する簡単なWorkerを作成します。

→ 例ではWorkerではなく、CoroutineWorkerを使用していますが、こうするだけでdoWorkはsuspend関数となり、内部ではCoroutineが使われるようになりますb

class TestWorker(
  appContext: Context,
  workerParams: WorkerParameters
): CoroutineWorker(appContext, workerParams) {
  override suspend fun doWork(): Result {
    repeat(10) {
      Log.d("TestWorker", it.plus(1).toString())
      delay(1000L)
    }
    return Result.success()
  }
}

次にこのWorkerをRequestします。一度きりでネットワークに繋がってるときのみの制約にしています。

補足: 前述したようにWorkerをRequestするときは、一度きりと定期的で分かれます。

また、実行条件の制約をつける場合はConstraints.Builderで指定し、setConstraintsにsetしてあげる必要があります。

val testWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<TestWorker>()
  .setConstraints(
    Constraints.Builder()
     .setRequiredNetworkType(NetworkType.CONNECTED)
     .build()
   ).build()

最後に適当なボタンを用意して、enqueueするようにして、実行してみましょう 途中でオフラインにしてみます。

f:id:qurangumio:20210907150527p:plain
実行中にオフラインにして、キャンセルされてる

Workerがキャンセルされてることが確認できますね。 このとき、最近機能が充実してAndroid StudioのApp Inspectionで覗くことができるので、見てみましょう!!

App Inspectionとは、RoomやWorkManagerといったdebugをしづらいものをAndroid Studioがdebugしやすいように用意してくれたサポートtool郡のことを指します。

まずはDB Inspector

f:id:qurangumio:20210907150523p:plain
DB InspectorでWorkManagerが作成しているworkdbを見ている

どうやら、内部でWorkManager用にworkdbというのが内部で自動作成されていることがInspectorからわかりますね。これが先程の内部挙動で紹介したRequestされたWorkerを保持しておくために必要なDBということが予測できます。

色んなtableがありますが、試しにWorkSpecというtableを開いてみましょう!

名前の通り、各Workerの詳細情報を持っていることが伝わりますね。

f:id:qurangumio:20210907150534p:plain
workdbのWorkSpecテーブルを見ている

次にArctic Fox Canary 3で新たにWorkManagerのdebug用に追加された、Background Task Inspectorを見てみましょう。

f:id:qurangumio:20210907150538p:plain
Background Task Inspectorを見ている

最後のWorkerが途中でオフラインにされたWorkerなのですが、ENQUEUEDになっていることがわかります。 この状態でアプリをオンラインにして再度立ち上げたら、再度Workerが動き出して無事に処理されました!!

さいごに

いかがだったでしょうか? 今までWorkManagerとは関わる機会がなかったのですが、今回導入することをきっかけにキャッチアップをし、気になるところなど、学んだことを書いてみました。

mikan androidではこのように新しめな技術を気軽に試して、導入できるかどうかを検証するのも業務の一環です。まだまだやりたいことがたくさんあり、メンバーが足りないので少しでも興味を持った方は是非お話しましょうー!

herp.careers