※ChatGPTを使用して記事を作成しています。
「バックグラウンド処理?今どきはWorkManager一択でしょ!」
そんな甘い気持ちで手を出したのが運の尽きだった。
確かにドキュメントには「信頼性の高いスケジューラ」と書いてある。
だが、「スケジュールされる」と「実行される」は全然別の話だったのだ──。
今回は、WorkManagerを導入したはずが「永遠に実行されないワーカー」に悩まされた数ヶ月を振り返ります。
導入の背景:バッテリーセーバー時も処理したい
とあるクライアントアプリで、ユーザーの利用ログを定期的に送信する要件があった。
- バックグラウンドでも動作する必要がある
- OSバージョンによらず安定して送信したい
- ネットワーク接続時のみ実行すれば良い
この要件にぴったりだと思ったのが、Jetpack WorkManagerだった。
WorkManagerは、OSの制限下でもバックグラウンドジョブを安定的に実行できるJetpackライブラリです。
この文句を信じて、導入を決定。
実装:定期ログ送信ワーカーを作った
class LogUploadWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
val logs = fetchPendingLogs()
return if (uploadLogs(logs)) {
Result.success()
} else {
Result.retry()
}
}
}
そして、以下のように8時間ごとのPeriodicWorkを登録。
val request = PeriodicWorkRequestBuilder<LogUploadWorker>(8, TimeUnit.HOURS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"LogUpload",
ExistingPeriodicWorkPolicy.KEEP,
request
)
これで完璧だと思った。
でも、全然動かない。
何が起きたか:登録はされてる。けど…実行されない!
Enqueued work: LogUpload
State: ENQUEUED
だが、アプリを放置しても、ネット接続があっても、バッテリーが十分でも、ログ送信が一度も実行されない。
当然、doWork()
も一切呼ばれない。
しかも、デバイスを変えると実行されたりする。
条件を変えても実行されたりされなかったり、挙動が安定しない。
本当の原因たち:WorkManagerの仕様に翻弄される
後からわかったのは、WorkManagerには複数の罠があるということ。
⚠️ 罠①:PeriodicWorkは最低15分間隔、かつ正確な間隔は保証されない
公式ドキュメントにも小さく書いてある:
「The minimum repeat interval that can be defined is 15 minutes. WorkManager does not guarantee exact timing.」
つまり、8時間ごとに実行されるとは限らない。
次回実行はシステムが決める。バッテリーや他の制約次第で、1日遅れることもある。
⚠️ 罠②:バッテリーセーバー中はWorkManagerが一切実行されないことがある(OS依存)
特にAndroid 9以降、バッテリーセーバー中はDozeモードによってジョブが制限される。
この時、Constraintsを満たしていてもジョブは実行されない。
私の検証端末(Pixel 3a)は、夜間にバッテリーセーバーが自動ONになっていたため、WorkManagerが一切動かなくなっていた。
⚠️ 罠③:デバイスメーカーの独自実装で勝手にスケジューラが殺される
特に中華系メーカー(Xiaomi、Huawei、OPPOなど)の端末では、WorkManagerのバックエンド(JobSchedulerやAlarmManager)がOSによって止められることがある。
ログにはエラーも出ず、「実行されない」だけ。
問題の再現と原因特定にかかった日数:30日以上
WorkManagerの「動くかもしれない」「動かないかもしれない」という非決定的な挙動が、原因の特定を非常に難しくした。
- ある端末では毎回実行される
- 別の端末では一度も動かない
- 機内モードON→OFFで動くこともある
- OSアップデートで挙動が変わる
正直、呪われているとしか思えなかった。
対処法:WorkManagerを使いこなすための鉄則
この地獄を抜け出すために、以下の対応を取った。
✅ 対策①:Constraintsとバックエンド挙動の理解
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build()
上記のようなConstraintsは便利だが、複雑にしすぎると一生満たされない。
可能な限りシンプルに、かつOSごとの動作確認を行う。
✅ 対策②:テスト用にOneTimeWorkRequestを使って挙動を確認
開発時は以下のような一時的なWorkRequestを使って、必ずdoWork()
が実行されるか確認。
val request = OneTimeWorkRequestBuilder<LogUploadWorker>().build()
WorkManager.getInstance(context).enqueue(request)
これでワーカーの登録や処理が正しく動くかどうかを明示的に検証できる。
✅ 対策③:永続ログを取り、doWork()の呼び出し回数を分析
端末内に「いつワーカーが実行されたか」のログを記録。
開発チーム全員で実機テストを行い、環境ごとの実行傾向を把握。
補足:JobSchedulerに切り替えようとしたら?
「WorkManager面倒だから、JobSchedulerに戻したら?」という意見も出た。
だがJobSchedulerはAPI21以降限定で、他の制約も多い。
また、WorkManagerは内部でJobSchedulerやAlarmManagerを自動で使い分けてくれるため、
「動かない原因を避けられる」とは限らない。
教訓:ワーカーは登録して終わりじゃない。走るまでが勝負
今回の事件を通じて学んだのは、WorkManagerは非常に強力な反面、環境に大きく依存するライブラリであるということ。
- スマホの設定
- OSバージョン
- メーカーの独自カスタマイズ
- ユーザーが気づかずONにしたバッテリーセーバー
「なぜ動かないか」よりも「どう動かすか」の工夫が必要だった。
まとめ:信頼性の裏には複雑な現実がある
Jetpackライブラリは便利で安全。
だが、その「安全」はOSやメーカーの協力あってのもの。
WorkManagerは、信頼性を「目指す」ライブラリであって、何もしなくても絶対動くわけではない。
結局、バッテリーやネットワークやスケジューラなど、システムの状態に強く依存するのがAndroid。
その上に構築される非同期処理には、動作保証よりも“監視と補完”が求められる。