愿历尽千帆 归来仍少年

Android S Doze模式

字数统计: 3,137阅读时长: 13 min
2022/04/19

前言

基于Android S梳理下Doze 机制

认识 Doze

如果用户未插接设备的电源,在屏幕关闭的情况下,让设备在一段时间内保持不活动状态,那么设备就会进入Doze 模式。
在Doze 模式下,系统会尝试通过限制应用访问占用大量网络和 CPU 资源的服务来节省电量。它还会阻止应用访问网络,并延迟其作业、同步和标准闹钟。
系统会定期退出Doze 模式一小段时间,让应用完成其延迟的活动。在此维护期内,系统会运行所有待处理的同步、作业和闹钟,并允许应用访问网络。


(摘自 https://developer.android.google.cn/training/monitoring-device-state/doze-standby)

关于上面这张图的理解:

  1. 当设备灭屏且未充电下,一段时间后会先进入图中的第一个Doze 即light doze 状态。
  2. light doze 阶段对应用限制措施相比deep doze 会少一些,light doze阶段同样有窗口期,窗口期内会解除对应用的限制措施。
  3. 一段时间后,如设备处于静止状态,会进入到deep doze,从图中可以看到deep doze 阶段对应用的限制措施会有所增加,比如增加了Alarm、Wifi扫描等限制。
  4. 在deep doze 阶段,如没有外在条件改变比如位置移动、亮屏等,会在deep doze 和窗口期之间循环切换。

应用行为限制

deep doze 阶段对应用的行为限制如下:

暂停访问网络。
系统忽略唤醒锁定。
标准 AlarmManager 闹钟(包括 setExact() 和 setWindow())推迟到下一个维护期。
如果您需要设置在设备处于低电耗模式时触发的闹钟,请使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle()。
使用 setAlarmClock() 设置的闹钟将继续正常触发,系统会在这些闹钟触发之前不久退出低电耗模式。
系统不执行 WLAN 扫描。
系统不允许运行同步适配器。
系统不允许运行 JobScheduler。

(引用自 https://developer.android.google.cn/training/monitoring-device-state/doze-standby)
而对于应用进入light doze状态的话,应用限制会减少很多,只有网络限制、不允许运行同步适配器、不允许运行 JobScheduler这三种类型的限制措施,同时窗口间隔会更短。

Doze 状态集

deep doze 和light doze 的关系

  • 进入deep doze 状态前提之一是设备已经静止了一段时间,而进入light doze 则不需要这个条件。
  • deep doze 和light doze 可以在设备中同时运行,简单来说就是在进入deep之前,两个状态机的代码都会执行。
  • 设备进入deep doze 状态之前必须先经过light doze。
  • 如果设备进入了deep doze,那么light doze 的状态机代码就不会跑了。

下面介绍下deep doze 和light doze 的状态集
Deep doze
enter description here
Light doze
enter description here

Doze 状态机

Light Doze 状态机

light doze 的代码状态机
enter description here

Light doze 图形状态机
enter description here

下面我们将尽可能用通俗易懂的话来解释上面这张light doze 图形状态机

  1. 假设用户正在使用手机,此时停留在Active 状态。
  2. 用户使用一段时间后,关闭了屏幕且未在充电,则进入LIGHT_STATE_INACTIVE 状态。
  3. 3 分钟之后进入到PRE_IDLE 状态,这个阶段处理未完成的事务。
  4. 5 分钟之后会进入到LIGHT_STATE_IDLE 状态,这个时候会根据当前设备是否联网而选择进入不同的状态,有网络的话则进入到MAINTANCE 状态,否则进入WAITING_FOR_NETWORK 状态。
  5. 设备有网络的情况下,状态会在IDLE 和 MAINTANCE 之间交替切换。

PS:停留在light idle 时长并不会随着转换次数N的增加而无限增长,会受到最大值DEFAULT_LIGHT_MAX_IDLE_TIMEOUT = 15 min的限制

Deep Doze 状态机

Deep doze 的状态机相较于light doze 而言稍显复杂一些
enter description here

Deep doze 图形状态机
enter description here

下面我们将尽可能用通俗易懂的话来解释上面这张deep doze 图形状态机

  1. 假设用户正在使用手机,此时停留在Active 状态。
  2. 用户使用一段时间后,关闭了屏幕且未在充电,设备将很快进入InActive 状态。
  3. 过了30 min之后,进入Idle_pending状态并停留5 min。
  4. 5 min后进入Sensing 状态,这个阶段会检测设备是否有转向变化。
  5. 没有转向变化的话,会进入到Locating 状态并停留30s,这个阶段会检测位置是否变化。
  6. 位置没有变化的话,30s 后将进入Idle 状态即deep doze。
  7. Idle 状态停留60乘以2的N次方时间后(N是Idle 和窗口期转换的次数,初始值0),会进入到窗口期,窗口期内应用的限制措施会被解除。
    在窗口期停留5乘以2的N次方时间(N是Idle 和窗口期转换的次数,初始值0)后,会再次进入Idle,此后如没有外在条件变化的话,则会在idle 和窗口期之间交替切换。

PS:停留在deep idle 时长并不会随着转换次数N的增加而无限增长,会受到最大值DEFAULT_MAX_IDLE_TIMEOUT = 6 h的限制

Q:进入到deep Idle 状态后,是如何限制应用行为的?
A:stepIdleStateLocked函数扮演了重要的角色,主要用于切换状态机,
STATE_IDLE_MAINTENANCE 切换到STATE_IDLE 后,会发一个MSG_REPORT_IDLE_ON消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@VisibleForTesting
void stepIdleStateLocked(String reason) {
if (DEBUG) Slog.d(TAG, "stepIdleStateLocked: mState=" + mState);
EventLogTags.writeDeviceIdleStep();
//....
switch (mState) {
//....
case STATE_IDLE_MAINTENANCE:
//Doze 相关状态的停留时间都是通过Alarm 的超时来实现的
//超出mNextIdleDelay时间后,检测条件满足的话会再次进入STATE_IDLE_MAINTENANCE
scheduleAlarmLocked(mNextIdleDelay, true);
if (DEBUG) Slog.d(TAG, "Moved to STATE_IDLE. Next alarm in " + mNextIdleDelay +" ms.");
//Idle 状态停留的时间, IDLE_FACTO为2,所以是2的N次方
mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
if (DEBUG) Slog.d(TAG, "Setting mNextIdleDelay = " + mNextIdleDelay);
mIdleStartTime = SystemClock.elapsedRealtime();
mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);
//首次的话mNextIdleDelay是0,所以首次Idle 停留时间就是IDLE_TIMEOUT
//即60 * 60 * 1000L(60min)
if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {
mNextIdleDelay = mConstants.IDLE_TIMEOUT;
}
moveToStateLocked(STATE_IDLE, reason);
//进入到Idle 状态后,light 状态就不会运行了并取消light相关的alram超时
if (mLightState != LIGHT_STATE_OVERRIDE) {
mLightState = LIGHT_STATE_OVERRIDE;
cancelLightAlarmLocked();
}
addEvent(EVENT_DEEP_IDLE, null);
mGoingIdleWakeLock.acquire();
//发送 REPORT_IDLE_ON消息
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
break;
//...
}
}

进入到处理MSG_REPORT_IDLE_ON 消息环节,这个环节主要是通知其它模块起来做限制措施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void handleMessage(Message msg) {
if (DEBUG) Slog.d(TAG, "handleMessage(" + msg.what + ")");
switch (msg.what) {
//....
case MSG_REPORT_IDLE_ON:
case MSG_REPORT_IDLE_ON_LIGHT: {
// mGoingIdleWakeLock is held at this point
EventLogTags.writeDeviceIdleOnStart();
final boolean deepChanged;
final boolean lightChanged;
if (msg.what == MSG_REPORT_IDLE_ON) {
deepChanged = mLocalPowerManager.setDeviceIdleMode(true);
lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);
} else {
//触发powerManager模块调用setDeviceIdleMode做wakelock的释放
deepChanged = mLocalPowerManager.setDeviceIdleMode(false);
lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true);
}
try {
//触发NetworkPolicy模块调用setDeviceIdleMode做应用的网络限制
mNetworkPolicyManager.setDeviceIdleMode(true);
mBatteryStats.noteDeviceIdleMode(msg.what == MSG_REPORT_IDLE_ON
? BatteryStats.DEVICE_IDLE_MODE_DEEP
: BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid());
} catch (RemoteException e) {
}
//....
} break;
//....
}

同样的,当切换到窗口期后会发一个MSG_REPORT_IDLE_OFF 消息,并通知PowerMS、Network等模块解除对应用的限制措施。

Doze 白名单

当设备处于Idle 状态下的时候,位于白名单下的应用行为不受doze 机制限制。
当前系统维护了三种类型的白名单
1.system-excidle(System app)
这种名单只针对doze 模式起作用,对于应用处于idle情况下不会生效。

1
2
3
4
/**
* Package names the system has white-listed to opt out of power save restrictios, * except for device idle mode.
*/
private final ArrayMap<String, Integer> mPowerSaveWhitelistAppsExceptIdle = new ArrayMap<>();

2.system(System app)
这种类型的名单对doze 和App standby模式均会起效,用户无法在电池优化界面手动修改其状态。

1
2
3
4
/**
* Package names the system has white-listed to opt out of power save restriction * s for all modes.
*/
private final ArrayMap<String, Integer> mPowerSaveWhitelistApps = new ArrayMap<>();

3.user
这种类型的名单对doze 和App standby模式均会起效

1
2
3
4
/**
* Package names the user has white-listed to opt out of power save restrictions.
*/
private final ArrayMap<String, Integer> mPowerSaveWhitelistUserApps = new ArrayMap<>();

User 这种白名单通常可以用户自行设置,设置中的电池优化选项对应的正是这种类型,如果希望某个应用不走doze,则将该应用的电池优化关闭即可。

上面三种类型可以通过dumpsys deviceidle whitelist 命令查询
enter description here
上图可以看到微信和飞书在user 白名单中,也就说doze 下不会对这两个应用的行为比如联网进行限制。至于为何这两个三方应用会加到白名单中,是因为它们没有使用任何系统级推送通道,但是由于它们的用户群体比较庞大,最好是加入到白名单,以免出现灭屏后收不到消息引起客诉。

加到白名单中的应用会通过层层调用到PMS、Network、Alarm等模块中,这些模块中对白名单应用会正常放行,不会做特殊的限制措施。

调用addPowerSaveWhitelistApps 后的时序如下:
enter description here

常见问题 Q&A

Q:Doze 和省电模式(Battery saver) 差异?
A:省电模式是Google 自Android L版本引入的省电策略
省电模式的限制措施以及触发条件:
1.应用处于后台且不在白名单中的话,禁止联网。
2.电量低于一定阈值(默认15%)且未充电情况下则自动触发,高于一定阈值或充上电后会自动关闭,用户可以通过设置中的开关项主动打开,另外省电模式的触发阈值也可手动调整.

归纳下来:
1.两者触发机制不同
2.限制措施有部分重叠,比如都会限制应用联网
3.共用白名单deviceidle.xml,所以应用是否进入省电模式同样可以在电池优化界面设置。

Q:Doze 和应用待机(App standby) 差异?
A:App Standby是一种电池管理技术,根据应用最近使用时间和使用频率分为不同的组,开发者选项中的待机应用菜单,对应用使用jobs,alarm,network进行不同程度限制,达到省电的目的。
当用户不触摸使用应用程序一段时间时,该应用程序处于App Standby状态,系统将把该App标志为空闲状态。除非触发以下任意条件,应用程序将退出App Standby状态:

The user explicitly launches the app.
The app has a process currently in the foreground (either as an activity or foreground service, or in use by another activity or foreground service).
The app generates a notification that users see on the lock screen or in the notification tray.
The app is an active device admin app (for example, a device policy controller). Although they generally run in the background, device admin apps never enter App Standby because they must remain available to receive policy from a server at any time.
(引用自 https://developer.android.com/training/monitoring-device-state/doze-standby)
归纳:
1.两者进入条件不同,比如Doze 前提条件之一是灭屏,而App standby 则不需要灭屏。
2.共用白名单deviceidle.xml,除了system-excidle,因为system-excidle是只限制doze。

Q:Doze 和深度睡眠差异?
A:Doze 在sleep 之前,doze 阶段会解除应用的wakelock,限制联网,但是程序还是可以运行的。
但是设备一旦进入深度睡眠,进程会被冻结,CPU挂起。
还有一点差异是进入doze 状态不受wakelock 影响,也就是是不管当前是否存在wakelock 没有释放都不会影响设备进doze,doze 状态的进入只取决于超时阈值以及一堆传感器判断。但是进入深度睡眠之前,如果应用持有wakelock 则不会进入到深度睡眠模式。

Q:如何调试模拟Doze?
A: 本地可以通过如下命令进行调试验证

  1. 开启Doze
    adb shell dumpsys deviceidle enable
    或者在MTK平台上执行 adb shell setprop persist.config.AutoPowerModes 1
  2. 模拟移除电源
    adb shell dumpsys battery unplug
  3. 调试Doze状态
    adb shell dumpsys deviceidle step 每执行一次就切换一次状态
  4. Dump Doze 状态分析
    查询白名单情况:adb shell dumpsys deviceidle
  5. 开启Doze dubug 调试开关
    如需要本地调试验证问题的话,可以将DeviceIdleController.java文件中的
    private static final boolean DEBUG = false;

小结

关于Android S上的Doze 机制介绍到这里。

参考文献

https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=en

CATALOG
  1. 1. 前言
  2. 2. 认识 Doze
  3. 3. 应用行为限制
  4. 4. Doze 状态集
  5. 5. Doze 状态机
    1. 5.1. Light Doze 状态机
    2. 5.2. Deep Doze 状态机
  6. 6. Doze 白名单
  7. 7. 常见问题 Q&A
  8. 8. 小结
  9. 9. 参考文献