実践 WorkManager

Atsushi Sakai
Feb 12 · 16 min read

この記事は、DroidKaigi:2019で登壇した「実践 WorkManager」という発表内容を、必要な時に参照しやすいように、また当日発表を見ることができなかった方のためにスクリプトを記事に書き起こして加筆などしたものになります。発表時の資料は以下になります。

登壇時に収録された動画も公開されたので、そちらの方でも同じ内容を把握することができます。お好みに合わせてご利用ください。

また、この記事で扱われている内容の全ては、私が開発をしている「みてね」というアプリに実装されているものになります。是非、ダウンロードして体験してみてください。

WorkManagerを使って解決したかった課題

写真の表示が遅い

分析: どのぐらい遅いのか?

New Relic Mobileで計測した画像のResponse Time比較

USでは0.71秒、JPでは0.17秒という平均的な数字が出ているのがわかります。この数字を利用して、日本とアメリカのユーザーにおける平均的なUXの差を体感できるように比較したのが以下の動画です。

左側の動画ではUSにおけるレスポンスタイム 0.71sec. を、右側の動画ではJPにおけるレスポンスタム 0.17sec. を画像表示前に、わざと挿入してそのレスポンスタイムを体感できるように再現しています。これが、日本とアメリカにおける平均的なUXの差なわけです。

ただ、これはあくまで平均的な数字に基づいた再現動画なのですが、現実はもっとひどいものでした。次の動画をご覧ください。

これは、New Yorkに出張した際にWeWorkのとても品質の良いWi-Fiネットワークを使用してアクセスした際の実際の挙動を撮影した動画になります。この状況では、はっきり言って「アルバムアプリ」としては使い物にならないぐらいひどい状況であったことがわかると思います(これではさすがにレビューに書きたくもなる…)。

解決したいこと

  • 平均値0.54秒のUXの差を埋めると言うこと。
  • 世界中のどの国でも日本と同じ0.17秒以下の体験を実現すること、です。

WorkManagerを採用するまでの経緯

調査によって根本的な問題の原因は把握できていました。

インフラ側の話になってしまいますが、上記画像の通り、アメリカ国内でのCDNとアプリの通信は十分速いのですが、ストレージサーバーが国内に存在しているため、単に距離的に遠いためCDNに画像を受け渡す通信がとても遅いことが根本的な原因でした。

CDNになる早でキャッシュさせれば良いのでは?

Androidアプリの起動前に画像をキャッシュしておけば良いのでは?

では、Androidアプリでそのようなことを行う方針の上で、その実装に対しての大雑把な要求をまとめてみます。

  1. アプリが起動していない時に処理を行うこと。
  2. Wi-Fi接続している時のみ処理を行うこと。
  3. バッテリーが充分にあるときに処理を行うこと。
  4. 新しいキャッシュを取得するために1日に1回くらいは処理を行うこと。
  5. minSdkVersion 19でも動くプログラムであること。

WorkManagerとは?

  • バックグラウンドジョブを実行・管理するためのAndroid Jetpackコンポーネント
  • アプリが終了しても確実にタスクを終えたい処理を実行させる
  • 様々な制約(Constraints)を組み合わせてジョブの実行を制御することができる
  • API 14以降で同じコードが動く
    API 23+ではJobSchedulerが使われる
    API 14~22ではAlarmManager + BroadcastReceiverが使われる

これらのWorkManagerの実装は、私たちが要求していた条件に合致する部分が多かったため、WorkManagerを画像キャッシュに使おうという結論に至りました。

実装

私たちのアプリにはSynchronizerと呼ばれているアルバムに表示するすべてのメタデータをクラウド上のデータベースと端末内のSQLiteで同期する仕組みがあります。そこを起点としてWorkManagerを使ってバックグラウンドジョブの実行が行われるように実装を行いました。上記はそのシーケンスをまとめたものになります。このシーケンスのように動くプログラムを書き、アルバムに表示するための多くの画像をバックグラウンドで取得する実装を行いました。このシーケンスの中で重要なのが画像の中でテキストがハイライトされている ジョブをエンキューするという箇所と、ジョブを実行する箇所になります。それぞれ説明をしていきます。

Enqueue Job

  • WiFiに接続していること
  • バッテリーが充分にある、もしくは充電中である

という制約を満たした時にJobが動いて欲しいわけで、それを実現するコードがこちらになります。

Constraintsクラスを用いて、NetworkTypeとバッテリー・充電状態をBoolやEnumで指定するだけです。

また、24時間に1回実行させたいという要求もありました。

PeriodicWorkRequest

PeriodicWorkRequestというクラスを用いてWorkManagerにJobの実行をリクエストすると以下のようなことが実現できます。

  • 繰り返し同じJobをInteval毎に実行させる
  • 最初の実行は即時もしくは制約を満たした直後に行われる

https://developer.android.com/reference/androidx/work/PeriodicWorkRequest

そして最後に、先ほど作ったPeriodicWorkRequestを enqueueします。

enqueueUniquePeriodicWork

ここで、このメソッドを使ってWorkerをenqueueすると以下のようなことが実現できます。

  • ユニークな名前の付いたPeriodicWorkRequestを常にひとつだけenqueueされている状態を保証させる。
  • 同時にふたつのJobが並列に動く必要が無い場合に最適なenqueue方法。

https://developer.android.com/reference/androidx/work/WorkManager#enqueueUniquePeriodicWork

Execute Job

  • SQLiteからレコードを取得
  • URLにアクセスし画像を取得
  • Glideに取得した画像をキャッシュ

実装時のTIPSや注意事項

Workerは冪等性を担保した処理を書く

Jobが失敗した時にはほとんどの場合、WorkManagerはそのJobをリトライしてきます。そういったときのために、同じパラメータで複数回Jobがenqueueされ実行されても常に同じ結果・状態が返るように実装するようにしましょう。

例えば、DBにデータを書きながら実行を進めていくようなJobが途中で失敗した場合、再度リトライするときに以前書き込んでしまったレコードが影響して意図しない状態にならないように実装しておくと良いでしょう。

リトライの挙動を理解する

BackoffPolicy
デフォルトで BackoffPolicy.EXPONENTIALという設定がされているのですが、これは、リトライ間隔を指数関数的に増加させていくような設定です。大抵の場合はこの設定で問題ないですが、最終的に数日後とかにJobがリトライされる可能性があることは理解しておくと良いでしょう。

リトライ回数
また、リトライ回数の最大値を設定することができないのでこちらも注意してください。回数制限したい場合は、Workerクラス内でgetRunAttemptCount を呼ぶと現在の試行回数を取得可能なのでこれを使って、任意の回数に達したらFailureさせてRetryされるのを防ぐと良いでしょう。

意図せぬリトライ予防
そして、そもそも絶対に成功しないパターンなどが分かっている場合は、意図しないリトライが行われないようにエラーハンドリングしておく事も重要です。

これら挙動を理解をしておかないと、例えば意図せずリトライをしすぎてAPI実行しまくってサーバーに負荷をかけてしまうとか、障害を引き起こしてしまう原因にもなりますので注意して下さい。

Workerのコードをアップデートするときには注意

InputDataの型やキーを無闇に変更してしまうと、前のバージョンでenqueueしたJobがアップデート後に実行されて、Workerの実装が変わって意図しないエラーになる。また、Workerのクラス名を変更した場合は、アップデート前にenqueueしたJobは実行時にinitできず、すぐにFailure扱いになり一切実行されなくなってしまいます。

Workerはタイムアウトされる

これは、依存しているJobSchedulerやAlarmManagerの制約でもありますが、Job毎に10分間のタイムアウト値が存在しています、10分を過ぎるとstopのSignalが送られて処理が中断されるので注意してください。

長くなりそうな処理を実行するときは、処理の単位を細分化し複数のWorkerをチェーンするように構成するとよいでしょう。

こちらはドキュメントから引用したものなのですが、AというJobが終了した後にB、Bが終了した後にD, Eがチェーンして実行される、というような構成を簡単に作ることが可能です。

この機構を使ってタイムアウトから解放される、と言ったことも可能ですので考慮すると良いと思います。

Workerをcancelする時の挙動に注意

  • 動いているWorkerがすぐさま停止されるわけではない。

ということです。キャンセルが要求された際には

  • Worker#onStoppedが実行される
  • Worker#isStoppedでtrueが返るようになる

という動きになりますので、キャンセル時に何か後処理などをしたい場合は以下で実現できないかを考慮しましょう。

QAが難しい

テストについて

どのようにテストを記述するのか

WorkManagerTestInitHelperについて

  • Test実行時のContextを使ってシングルトンなWorkManagerをinitさせる。
  • TestDriverを生成し、Constraints(制約)を偽装し、Workerをすぐに実行することができる。

これを使って、例えば以下のように記述します。

一行目で initializeTestWorkManager を実行することでテスト用のWorkManagerのシングルトンなインスタンスが内部で生成されます。

次に二行目で、自分たちが作ったWorkerをenqueueします。

その直後にtestDriverを生成し、setAllConstraintsMetというメソッドを呼ぶことでConstraintsが全て満たされたという状況が偽装されるので、この直後にWorkerのコードが走り始めます。

しかし、実はテストに関しては調べてみたところ、ISSUE TrackerにISSUEが上がっており、WorkManagerのテストはAndroid Instrumentation Testでテスト走らせてくれ、ということでした…!(発表前日の深夜に発覚した)

We never supported Roboelectric. All WorkManager tests need to run as an Android instrumentation test.

成果

右側は実際にリリースされているバイナリを使った挙動ですが、キャッシュを行なった結果、プログレスが一切表示されないで表示されていることがわかります。

これにより、UI上のロード遅延は一切なくなり、またユーザーが表示したいと思う前にWorkManagerによってキャッシュが生成されているため、レスポンスタイムを意識する必要もなくなったので、世界中で同じ体験を提供できるようになったとも言えます。

さらに、地下鉄などの場所でオフラインであっても写真をより確実に見れるようになるという副次的な効果も実はありました。

残された課題

端末側のリソースに頼りすぎ

そのため、バックグラウンドジョブの挙動をユーザーが任意に調整できるようなUIを提供し、少しでも安心してもらえるようにしています。

リクエストに紐付かないCDNへの任意キャッシュ機能が無い問題

まとめ

WorkManagerは、発想次第で今回紹介したような細かいUX改善にも活かせるので、この記事がみなさんの課題解決の役に立てたら嬉しいです。

mixi developers

ミクシィグループのエンジニアやデザイナーによるブログです。

Atsushi Sakai

Written by

mixi developers

ミクシィグループのエンジニアやデザイナーによるブログです。

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade