※ChatGPTを使用して記事を作成しています。
スマホのバッテリーを異常に消費するアプリがある──。
それを作ってしまったのは、他でもない私でした。
当時の僕は「定期実行したいならAlarmManager使えばいいでしょ」と、安易に繰り返しタスクを設定。
その結果、ユーザーから「このアプリ入れたら電池が死ぬ」とクレーム殺到。
本記事では、Androidのバックグラウンド処理を甘く見た代償と、改善に至るまでの試行錯誤を赤裸々にお伝えします。
電池消費が「異常」と言われた日
ある日、Google Playのレビュー欄にこんなコメントが並び始めました。
「このアプリ入れたら半日でバッテリーが空になる」
「なんか裏でずっと動いてる?電池が減りすぎ」
「便利だけど電池消費がエグいので削除します」
心当たりが……ある。
開発中のあの機能──AlarmManagerで定期的にサーバーと通信する仕組み。
そのときの実装はこんな感じでした。
とりあえずAlarmManagerで定期実行!
当時の要件はこうでした。
- アプリが閉じていても定期的に通知を出す
- サーバーから新着データを取得してローカルに反映
- なるべくリアルタイムに近い(最低でも10分に1回)
で、当時の自分は以下のような設計にしていました
AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, MyReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
alarmMgr.setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 60 * 1000,
10 * 60 * 1000, // 10分ごと
pendingIntent
);
「AlarmManagerって便利だな〜」
なんて感心しながら、バッテリーやDozeモードの存在を完全に無視していたのです。
まさかの“Doze無視”とCPUウェイク地獄
しばらくして、クライアントからメール。
「このアプリ、他のと比べて異様にバッテリー消費してるって報告が相次いでます。調査をお願いします」
ログを見ると、夜中でも10分ごとにCPUが起きて通信している。
Android 6.0(Marshmallow)以降はDozeモードでバッテリー消費を抑えるようになっているはず…。
でも、AlarmManager.setRepeating()
はDozeモードを無視して発火することもあり得る(特にWAKEUP
を使っていた)。
しかも、通信処理は毎回WakeLockを取得していたので、結果として常にCPUがスリープから叩き起こされていたのです。
setRepeating()は過去の遺産!?
実は、setRepeating()
は現在では非推奨に近い扱いになっている(公式にdeprecatedではないが、Doze対応の面で実質使うべきでない)。
それを知らずに、ずっと同じ間隔で強制的に通信処理を行わせていたわけです。
さらに追い打ちをかけるように、通信失敗時にも即リトライをかけるロジックまで組み込んでいた僕。
つまり…
- 10分ごとのWakeUp Alarm
- 通信失敗でリトライ(最大3回)
- WakeLock取得 → CPU稼働継続
- 通信完了までバックグラウンドで回り続ける
これ、バッテリーが死ぬのも当然ですよね。
解決に向けて:JobSchedulerへの移行
そこで選択したのが、JobScheduler。
ComponentName componentName = new ComponentName(context, SyncJobService.class);
JobInfo jobInfo = new JobInfo.Builder(123, componentName)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setPeriodic(15 * 60 * 1000) // 最小周期(15分)
.setPersisted(true)
.build();
JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
scheduler.schedule(jobInfo);
JobSchedulerなら、以下のようなメリットがあります。
- Dozeモードにも対応
- OSの電池最適化制御に従う
- ネットワークの条件も指定可能
- 15分以上の周期で安定的に実行される
ただし、リアルタイム性が落ちる。
それでも、「バッテリーを犠牲にするか、更新の即時性を捨てるか」の天秤で、後者を選びました。
それでも足りない:WorkManagerへの再移行
しばらくして、APIレベルが上がるにつれて、JobSchedulerの制約も強くなっていく中、
Googleが推してきたのが WorkManager。
こちらはJobSchedulerのラッパーでありながら、
API 14以降に対応しつつ、内部で最適なスケジューリング手法を選んでくれる優れもの。
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(SyncWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"SyncWork",
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
);
この移行で、バッテリー消費は大幅に改善。
ユーザーからの不満も減り、レビュー平均も戻ってきました。
学びと今の僕のスタンス
この事件から得た教訓は大きく、今では次のような考え方をしています。
🔸 バッテリーと通信は“貴重な資源”
モバイル端末では「何もしてないようでしている」が最悪。
バッテリーも通信量も有限資源。
🔸 「今すぐ」が本当に必要かを見極める
即時性を求める前に、「10分遅れて困る人は誰か?」と考える。
たいていは、多少遅れても問題ない。
🔸 バックグラウンド処理は“OSの言うことを聞く”のが正解
自分でWakeLock握ってAlarm飛ばすより、WorkManagerやJobSchedulerに任せた方が正しい。
Androidはもう「自己管理」で動かす時代ではない。
まとめ
この失敗は、「ユーザーの電池を勝手に使い切ってしまった」という点で、個人的にはかなりの黒歴史です。
でも、それがあったからこそ、今は「本当に必要な処理だけを、OSと相談しながら走らせる」という設計ができるようになりました。
バッテリー消費に関しては「気づかれない」のが最上級の褒め言葉。
そのためにも、設計の段階で「これ、本当に今すぐやるべき処理か?」と自問自答するようにしています。