Edited at

Android M/NのDozeによる制限とバックグラウンドタスク実行に関するまとめ

More than 3 years have passed since last update.


個人的に一番重要なこと

Q. AlarmMangerで設定したアラームはDoze中に発火されるか?

A. 浅いDoze中は発火される。深いDoze中は発火されない。

深いDozeから抜けるアラームもAPI 23で追加されたが、Doze中は浅いDoze深いDozeにかかわらずネットワークアクセスが原則不可能1なので、これまで通りにAlarmManagerでなんでもできるというわけではなくなった。


Doze


Dozeの概要

簡単に言うと、Android端末が使われていないときにDoze(居眠り)状態に入ってバッテリー消費を抑えるための機能。

doze-diagram-1.png

※ 画像はAndroid 7.0 Behavior Changesより引用

端末が


  1. バッテリー駆動(つまり電源プラグに接続されていない)状態

  2. スクリーンOFFから一定時間経過

  3. かつ静止状態 (Stationary)2

ならば端末はDoze状態になり、ネットワークは遮断されCPU処理は次回メンテナンスウィンドウ (Maintenance Window)まで遅延される。

Android 6.0以降で動作するアプリは、ターゲットAPIレベルが23以降に指定されてビルドされているか否かにかかわらず、条件を満たせばDozeモードに突入する。


Dozeの種類

DozeはAndroid M (API 23)で初めて登場し、Android N (API 24)で強化された。

簡単に言うとMからあるDozeが深いDoze3で、Nで追加されたDozeが浅いDoze3である。Nには浅いDozeと深いDozeの2段階が存在する。


深いDoze

Android M (API 23)で初登場したDoze。

以下の条件を満たすと突入する。深いDozeに入る条件はNでも変わっていない。


突入条件


  • バッテリー駆動

  • スクリーンOFF

  • 静止状態(Stationary)

この状態で約30分後(後述)に突入。


脱出条件

上記のいずれかの条件が破られる。または定期的に訪れるメンテナンスウィンドウ(後述)。

なお、静止状態(Stationary)ではなくなるが依然としてバッテリー駆動かつスクリーンOFFの場合、N以降では深いDozeから浅いDozeへ移行する。

ちなみに、(ポケットに入っているなどの理由で)近接センサが働いている場合、Stationaryでなくても残りの条件が満たされればDozeへと移行しうる。


浅いDoze

Android N (API 24)で追加されたDozeの第1段階。

以下の条件を満たすと突入する。条件が満たされると第2段階の深いDozeに移行する。


突入条件


  • バッテリー駆動

  • スクリーンOFF

この状態で数分後(後述)に突入。Nでは静止状態(Stationary)を経なくてもDozeに入ることが分かる。

なお、いきなり深いDozeの条件を満たしていても、N以降では必ず浅いDozeを経て深いDozeへと移行する。(直接深いDozeに陥ることはない)


脱出条件

上記のいずれかの条件が破られる。または定期的に訪れるメンテナンスウィンドウ(後述)。


静止状態(Stationary)

静止状態(Stationary)の正確な定義は説明しづらい。端末によって違う可能性がある。

静止状態(Stationary)かどうかの判定に関して、Power Management には Significant Motion Sensor を利用すると解説されている。

Significant Motion Sensorは加速度センサなどの「動きを検知するセンサ」を複数まとめた仮想的なもので、端末によって異なる場合があるようだ。

また、DozeのStationaryの判定にSignificant Motion Sensorが必ず使われるというわけではなく、どのセンサを利用するかはconfig.xmlによって制御できる模様。こちらも端末によって異なる可能性がある。


Doze中の制限


浅いDozeの場合

ここで重要なのが、浅いDoze中は AlarmManager が利用可能であるということ、ネットワークは利用できないという事実である。


深いDozeの場合


  • 浅いDozeの制限すべて

  • Wake Lockの取得を無視


  • AlarmManagerを無視

  • GPSとWi-Fiのスキャンを停止

深いDozeにあってはAlarmManagerも利用できなくなる4


Doze状態に突入するまでの時間

Doze状態に突入するまでの時間は、公式ドキュメントには一定時間(for a certain time)とあるのみで、浅いDoze深いDozeいずれの場合も具体的な数値の言及がない。

以下は識者のブログを拝見したり自分でソースを読んだりした結果なので、不正確な場合は是非ご指摘or編集リクエストを送っていただきたく。


浅いDozeの場合

コードで追えていないが @chun_ryo 先生のAndroid Nで導入される「浅いDoze」についてによると、Nexus 5Xの場合、5分とのこと。


深いDozeの場合

DeviceIdleControllerを読む限り、STATE_INACTIVEになってから30分+αで突入するように読める。



  • STATE_INACTIVEscheduleAlarmLocked(mConstants.IDLE_AFTER_INACTIVE_TIMEOUT, false); // 30分


  • STATE_SENSINGからSTATE_IDLE_MAINTENANCEにfall throughしてscheduleAlarmLocked(mNextIdleDelay, true); // 第2引数により即座にIDLEに突入


メンテナンスウィンドウ

ペンディングになっている以下のジョブをすべて実行する。



  • SyncAdapterの同期


  • JobSchedulerのジョブ


  • AlarmManagerの処理


メンテナンスウィンドウに突入する頻度


浅いDozeの場合

コードで追えていないが @chun_ryo 先生のAndroid Nで導入される「浅いDoze」についてによると、Nexus 5Xの場合、5分、10分、15分と等間隔である模様。


深いDozeの場合

DeviceIdleController#stepIdleStateLocked()を読む限り、1時間に1回。

以後、倍々で2時間、4時間となっていくが、6時間間隔が上限のようだ。

// DeviceIdleController#stepIdleStateLocked()

case STATE_IDLE_MAINTENANCE:
scheduleAlarmLocked(mNextIdleDelay, true);
mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT)

// DeviceIdleController.Constants

IDLE_TIMEOUT = mParser.getLong(KEY_IDLE_TIMEOUT,
!COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L);
MAX_IDLE_TIMEOUT = mParser.getLong(KEY_MAX_IDLE_TIMEOUT,
!COMPRESS_TIME ? 6 * 60 * 60 * 1000L : 30 * 60 * 1000L);
IDLE_FACTOR = mParser.getFloat(KEY_IDLE_FACTOR,
2f);


Doze中にバックグラウンドタスクを実行する

Doze中はこれまで述べたような様々な制限がある。

特に既存のアプリがAlarmManagerで定期的にバックグラウンドタスクを実行している場合、M/N以降ではアプリの動作に重要な制限が課される可能性がある。

ここではDoze中にバックグラウンドタスクを実行する様々な方法についてまとめる。


AlarmManager#setAndAllowWhileIdle, setExactAndAllowWhileIdle

以下の2メソッドは、深いDoze中でも発火されるアラーム(PendingIntent)をセットすることができる。

メソッド名とシグネチャを見ると分かるように、これは元来のAlarmManagerにあったset, setExactに対応している。使い分けもまったく同様である。

このメソッド経由で発火されたアラーム(PendingIntent)は約10秒のWake Lockを取得できる。それ以上長い処理は、PendingIntentで起動されるService等の中で自前でWake Lockを取得する必要がある。

参考) WAKELOCKを取得し、SLEEP状態からWAKE状態へ遷移する

これらのメソッドは9分に1回以上呼び出すことができないAdapting your app to Dozeに明記されている。

これまでも何度か述べたように、Doze中はネットワークアクセスが不可能であり、それはこのメソッドで呼び出されたアラームに関しても同様である。AlarmManagerでネットワークアクセスを行っている場合は他の戦略を採る必要がある。


AlarmManager#setAlarmClock

API 21で紹介された次のメソッドは、深いDoze中でも発火されるAlarm(PendingIntent)をセットすることができる上に、Wake Lockの取得、ネットワークアクセスなどほぼ自由にすることができる。

AlarmManager#setAlarmClock

より正確には、このAlarm発火直前に、端末がDozeから抜ける。

これは他のアプリもDozeから目覚め、多大なるCPU処理、ネットワークアクセスが復活することを意味する。

既存のアプリがみんなこのメソッドを万能薬であるが如く使用すると、Dozeのなかった時代と何ら違いがなくなってしまうので、Googleは今後審査時にピンポイントでこのメソッドの利用をチェックしてくる可能性がゼロではない。

ただし、自分の観測した範囲では、本メソッドを利用することに関する制限や罰則事項に関するGoogle公式のドキュメントは一切確認できなかった。

したがって、現時点で本メソッドの利用を思いとどまるだけの明確な理由はない。ただ、前述のとおり、個人的には将来的な規制は確定的であると考えている。

注意点: このメソッドで設定したアラームは実際の目覚ましとして使うアラームのようにアイコンが表示される。

setAlarmClock.png


以下余談

ちなみに、void setAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation) の第一引数のAlarmClockInfoのコンストラクタはAlarmManager.AlarmClockInfo (long triggerTime, PendingIntent showIntent)のようにPendingIntentを取るが、これとsetAlarmClockに渡すPendingIntentは(本来)別物である。

javadocを見る限り、setAlarmClock()メソッドは「本当に目覚まし時計としてつかうアラーム」を設定することを意図されているようで、AlarmClockInfoに渡すPendingIntentはこのアラームが鳴った時に通知に現れてタップすることで時刻設定用のActivityを表示するために用意されているようだ。

かたや、setAlarmClock()の第2引数のPendingIntentは実際にアラーム発火時に実行されるタスクを表現するものである。

なお、ためしにAlarmClockInfoのコンストラクタに渡すPendingIntentをnullにしてみたり、アラーム発火時に実行されるServiceのPendingIntentを指定(要するに同じPendingIntent)してみたが、特に問題なく動作はするようである。


 FCM, GCMを使う

リアルタイムメッセージングサービスなど、端末がDoze中でも受信したメッセージを即座にユーザに通知するなど、リアルタイム性がアプリのコアバリューと認められる場合、FCM(旧GCM)を使うことができる。

Using GCM to Interact with Your App While the Device is Idle

その場合、メッセージをhigh-priorityで送信しなければならない。

high-priorityで送られたメッセージを受信した場合、そのアプリだけが一時的にWake Lockを取得し、ネットワークアクセスもできる。行儀の良い方法と言える。


JobScheduler

Android 5 (API 21)以降が利用できる場合はJobSchedulerの利用を検討する。

JobSchedulerは、独自のServiceを作ってそれをPendingIntentとして渡すといった煩雑な操作もなく、簡単にバックグラウンドジョブをスケジュールすることができる。

JobInfo.Builderのjavadocを参照すると、ジョブを実行するのに必要なネットワークを指定するためのsetRequiredNetworkTypeメソッドや、デバイスがバッテリー駆動かどうかでジョブを実行するかどうか決めるsetRequiresChargingなどの便利なメソッドが豊富に提供されている。

ただしJobSchedulerそのものにDozeから強制的脱出する機能等はなく、ジョブはあくまでメンテナンスウィンドウで処理されることになる。


アプリをホワイトリストに入れる

これらのいずれの対策もアプリの要件を満たさない場合、アプリをDozeのホワイトリスト(Whitelist)に入れてDozeの制限を受けないようにすることができる。

1) ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGSアクションで一覧からユーザにアプリを選んでもらってDozeを無効にしてもらう

// API 23 or above

startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));

最適化していないアプリ.jpg

2) REQUEST_IGNORE_BATTERY_OPTIMIZATIONS パーミッションを要求した上で ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS アクションでユーザにこのアプリをDoze無効にしてもらう。

<!-- AndroidManifest.xml -->

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

Intent intent = new Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);

intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);

電池の最適化を無視.jpg

なお、いずれのケースもユーザがいつでも設定画面からDozeのオン/オフを切り替えられることを忘れてはならない。

アプリからは適宜 PowerManager.isIgnoringBatteryOptimizations() メソッドを利用して、アプリがすでにDoze無効かどうかを調べること。

PowerManager powerManager = getSystemService(PowerManager.class);

if (!powerManager.isIgnoringBatteryOptimizations(getPackageName())) {
// request disabling doze
}

ちなみに、ホワイトリストはどんなアプリでもやってよいわけではなく、Acceptable Use Cases for WhitelistingAcceptable Use Cases for Whitelisting にどのようなアプリがホワイトリストへの追加を要求してもよいかの表がある。

端的に言うと


  • アプリのコア機能が「スケジュールを正確に通知すること」や「(ウェアラブル等の)外部機器と常に通信する必要がある」のように、Dozeを無効にする必然性がある

  • かつFCM, GCMが利用できない事情がある

ケース以外は非推奨のようだ。


Doze状態の切り替わりを検知する

アプリ内でDozeに出たり入ったりすることを検知するには ACTION_DEVICE_IDLE_MODE_CHANGED アクションと、PowerManager#isDeviceIdleMode() メソッドを利用する。

// create BroadcastReceiver

public class DozeStateReceiver extends BroadcastReceiver {
private static final String TAG = DozeStateReceiver.class.getSimpleName();

@Override
public void onReceive(Context context, Intent intent) {
PowerManager powerManager = context.getSystemService(PowerManager.class);
boolean isDoze = powerManager.isDeviceIdleMode();
Log.d(TAG, "isDoze: " + isDoze);
}
}

// your Activity

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

IntentFilter intentFilter = new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
registerReceiver(new DozeStateReceiver(), intentFilter);
}

ただし、自分で試した範囲では深いDozeしか検知できないようだ。

また、注意点として、このBroadcastReceiverはAndroidManifestから登録するとなぜか動かないようだ。

ActivityなりServiceなりでコードでregisterReceiverしよう。


Doze状態をエミュレートする


深いDoze

1) バッテリー駆動状態のエミュレート

$ adb shell dumpsys battery unplug

2) Dozeをエミュレートするdeviceidleコマンドを有効化

$ adb shell dumpsys deviceidle enable

3) スクリーンをOFFにする

4) 以後、stepを実行するごとに状態が切り替わるので IDLE にする

$ adb shell dumpsys deviceidle step

Stepped to deep: IDLE_PENDING

$ adb shell dumpsys deviceidle step
Stepped to deep: SENSING

$ adb shell dumpsys deviceidle step
Stepped to deep: LOCATING

$ adb shell dumpsys deviceidle step
Stepped to deep: IDLE

$ adb shell dumpsys deviceidle step
Stepped to deep: IDLE_MAINTENANCE

$ adb shell dumpsys deviceidle step
Stepped to deep: IDLE

$ adb shell dumpsys deviceidle step
Stepped to deep: IDLE_MAINTENANCE

5) Dozeをエミュレートするdeviceidleコマンドを無効化

$ adb shell dumpsys deviceidle disable

6) バッテリー駆動状態を元に戻す

$ adb shell dumpsys battery reset

なお、スクリーンOFFせずに強制的にDozeを有効化するには deviceidle force-idle する

$ adb shell dumpsys deviceidle force-idle

$ adb shell dumpsys deviceidle | grep mState
mState=IDLE


浅いDoze

1) バッテリー駆動状態の切り替えは深いDoze同様

2) 浅いDozeを有効化

$ adb shell dumpsys deviceidle enable light

3) 以後、step lightするごとに浅いDozeの状態が切り替わる

$ adb shell dumpsys deviceidle step light

Stepped to light: IDLE

$ adb shell dumpsys deviceidle step light
Stepped to light: IDLE_MAINTENANCE

なお、深いDoze中の場合は step light は意味をなさないので adb shell dumpsys deviceidle disable で一度無効化しよう。


補足

数分以内に発火されるAlarmがAlarmManager等によってスケジュールされている場合、Dozeの状態切替コマンドが受け付けられないことが確認された。

その場合は deviceidle force-idle 等でスクリーンを有効にしたままDozeを強制ONにして、画面操作等から確認したいタスクを設定するなど工夫する必要がある。


参考資料





  1. setAlarmClockやFCMを使う方法が残されている。詳しくは本記事を読んで欲しい。 



  2. N以降の場合はStationaryでなくても(浅い)Dozeに突入する。詳しくは後述。 



  3. 浅いDoze/深いDozeという呼称に関しては便宜的なものであるが、(少なくともAndroidデベロッパーの間では)充分に人口に膾炙していると判断しこの表現を使用した。Android 7.0 Behavior ChangesOptimizing for Doze and App Standbyといった公式ドキュメントには「浅い」「深い」などの用語は確認できず、「第1段階(first level)」「第2段階(second level)」という表現があるのみである。余談だが、Diving into Doze Mode for Developersというエントリに深いDoze(Deep-Doze)と浅いDoze(Lightweight/Light-Doze)という言葉が明確に使われていて、内容も非常に参考になる(というかこのまとめはこのブログの要約に近い)ので適宜参照して欲しい。 



  4. 一部メソッドは利用可能。「Doze中のバックグラウンドタスク」にて詳述。 



  5. @chun_ryo さんの資料はどれも最高です!ありがとうございます。 



  6. 浅いDoze、深いDozeについて包括的にまとめられた良資料です。英語。 



  7. 公式ドキュメント