[toc]
基于 Android 7.1.1 源码,分析 doze 模式的原理!
0 综述
下面是 Android devoloper 网站对于 doze 模式的介绍:
从 Android 6.0(API 级别 23)开始,Android 引入了两个省电功能,可通过管理应用在设备未连接至电源时的行为方式为用户延长电池寿命。
低电耗模式(doze)通过在设备长时间处于闲置状态时推迟应用的后台 CPU 和网络操作来减少电池消耗。
应用待机模式可推迟用户近期未与之交互的应用的后台 Activity 的网络操作。
假设一个用户停止充电 (on battery: 利用电池供电),关闭屏幕 (screen off)。手机处于静止状态 (stationary)。保持以上条件一段时间之后,系统就会进入 Doze 模式。一旦进入 Doze 模式。系统就降低 (延缓) 应用对网络的訪问、以及对 CPU 的占用,来节省电池电量。
这一系列的文章先来分析下低电耗模式!
在低电耗 doze 模式下,应用会受到以下限制:
- 暂停访问 Network。
- 系统将忽略 Wake Locks。
- 标准 AlarmManager 闹铃(包括 setExact() 和 setWindow())推迟到 doze 模式的下一个 maintenance window 时间窗。
- 如果您需要设置在低电耗模式下触发的闹铃,请使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle()。
- 一般情况下,使用 setAlarmClock() 设置的闹铃将继续触发,但系统会在这些闹铃触发之前不久退出低电耗模式。
- 系统不执行 WiFi 扫描。
- 系统不允许运行 Sync 同步适配器。
- 系统不允许运行 JobScheduler。
doze 模式的核心实现在 DeviceIdleController 中,下面我们来看看 DeviceIdleController 的启动!
1 | private void startOtherServices() { |
启动 DeviceIdleController 的时机是在 SystemServer.startOtherServices 方法中!
1 new DeviceIdleController
1 | public DeviceIdleController(Context context) { |
配置文件:
位于 /data/system/deviceidle.xml 中!
1.1 new MyHandler
1 | final class MyHandler extends Handler { |
我们看到,MyHandler 绑定到了一个后台线程,他会在后台线程中做一些比较耗时的操作!
2 DeviceIdleController.onStart
接着是进入 onStart 方法!
1 |
|
SystemConfig 我们之前在分析 PMS 的启动的时候有涉及到,SystemConfig 会用来解析系统的配置信息!
这里涉及到了两个内部变量!
DeviceIdleController 提供了 2 中模式: mLightEnabled 和 mDeepEnabled!
deep idle 模式:会禁止 NetWork、Wakelock,还会禁止 Alarm。
light idle 模式:会禁止 NetWork、Wakelock,但是不会禁止 Alarm。
通过 config_enableAutoPowerModes 属性进行初始化,默认是 mLightEnabled = mDeepEnabled = false!
我们再来看下这里涉及到的重要集合:
mPowerSaveWhitelistAppsExceptIdle:保存在 power save 模式下可以后台运行,但在 doze 模式下不能后台运行的系统应用名单,packageName 和 appId 的映射!
mPowerSaveWhitelistSystemAppIdsExceptIdle:保存在 power save 模式下后台运行运行,但在 doze 模式下不能后台运行的系统应用名单,appId 和 true 的映射!
mPowerSaveWhitelistApps:保存在 power save 模式下可以后台运行的系统应用程序,packageName 和 appId 的映射!
mPowerSaveWhitelistSystemAppIds:保存在 power save 模式下可以后台运行的系统应用程序,appId 和 true 的映射!
2.1 new Constants
1 | public Constants(Handler handler, ContentResolver resolver) { |
可以看到 Constants 会监控数据库的变化,这里先做了一个 feature 的判断:PackageManager.FEATURE_WATCH,判断当前设备是否是可穿戴设备:watch!
因为这里我们关注的是手机设备,所以 Constants 监控的数据库是:Settings.Global.DEVICE_IDLE_CONSTANTS(device_idle_constants)
2.1.1 Constants.updateConstants
updateConstants 会从 device_idle_constants 数据库中读取属性值,初始化一些关键的常量信息,如果读取不到,会初始化为默认值!
1 | private void updateConstants() { |
这里的 COMPRESS_TIME 为 false!
1 | private static final boolean COMPRESS_TIME = false; |
2.1.2 Constants 常量
从上面可以看到,doze 模式下涉及到很多和时间相关的值,这里我们来解释一下:
2.1.2.1 Light Idle 常量
- LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT
light idle 时间属性,取值为 5 mins,当设备进入灭屏不充电的状态时,需要持续的时间!
1 | LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getLong( |
- LIGHT_PRE_IDLE_TIMEOUT
light idle 时间属性,取值为 10 mins,在进入 LIGHT_STATE_PRE_IDLE 阶段之前,如果判断设备有活跃操作执行的话,我们会设置一个 10mins 以后的 Alarm,等待这些操作执行完成,再进入 LIGHT_STATE_PRE_IDLE 阶段的处理!
1 | LIGHT_PRE_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_PRE_IDLE_TIMEOUT, |
- LIGHT_IDLE_TIMEOUT
light idle 时间属性,取值为 5 mins,表示 light idle 的最小持续时间,也是第一次进入 light idle 的持续时间!
1 | LIGHT_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_IDLE_TIMEOUT, |
- LIGHT_IDLE_FACTOR
light idle 时间属性,取值为 2f, light idle 持续时间因子!
1 | LIGHT_IDLE_FACTOR = mParser.getFloat(KEY_LIGHT_IDLE_FACTOR, 2f); |
- LIGHT_MAX_IDLE_TIMEOUT
light idle 时间属性,取值为 15 mins,表示 light idle 的最大持续时间!
1 | LIGHT_MAX_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_MAX_IDLE_TIMEOUT, |
- LIGHT_IDLE_MAINTENANCE_MIN_BUDGET
light idle 时间属性,取值为 1mins, light idle 的最小时间窗!
1 | LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = mParser.getLong( |
LIGHT_IDLE_MAINTENANCE_MAX_BUDGET
light idle 时间属性,取值为 5mins, light idle 的最大时间窗!1
2
3LIGHT_IDLE_MAINTENANCE_MAX_BUDGET = mParser.getLong(
KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET,
!COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L);MIN_LIGHT_MAINTENANCE_TIME
light idle 时间属性,取值为 5s, 用于判断是否提前退出时间窗!1
2
3MIN_LIGHT_MAINTENANCE_TIME = mParser.getLong(
KEY_MIN_LIGHT_MAINTENANCE_TIME,
!COMPRESS_TIME ? 5 * 1000L : 1 * 1000L);
2.1.2.2 Deep Idle 常量
MIN_DEEP_MAINTENANCE_TIME
deep idle 时间属性,取值为 30s, 用于判断是否提前退出时间窗!1
2
3MIN_DEEP_MAINTENANCE_TIME = mParser.getLong(
KEY_MIN_DEEP_MAINTENANCE_TIME,
!COMPRESS_TIME ? 30 * 1000L : 5 * 1000L);INACTIVE_TIMEOUT
deep idle 时间属性,取值为 30 mins,当设备的角度发生变化后,会回到 STATE_ACTIVE 状态,当下次满足灭屏不充电时,需要持续该时间才能进入 STATE_INACTIVE 状态;第一次进入满足灭屏不充电时,时间值也由 INACTIVE_TIMEOUT 设置 !!
1 | long inactiveTimeoutDefault = (mHasWatch ? 15 : 30) * 60 * 1000L; |
SENSING_TIMEOUT
deep idle 时间属性,取值为 4 mins!1
2SENSING_TIMEOUT = mParser.getLong(KEY_SENSING_TIMEOUT,
!DEBUG ? 4 * 60 * 1000L : 60 * 1000L);LOCATING_TIMEOUT
deep idle 时间属性,取值为 30s,当处于定位阶段时,要持续的时间!1
2LOCATING_TIMEOUT = mParser.getLong(KEY_LOCATING_TIMEOUT,
!DEBUG ? 30 * 1000L : 15 * 1000L);LOCATION_ACCURACY
deep idle 定位属性,取值为 20meter,定位精度!1
LOCATION_ACCURACY = mParser.getFloat(KEY_LOCATION_ACCURACY, 20);
MOTION_INACTIVE_TIMEOUT
deep idle 时间属性,取值为 10mins,当 sensor 被触发后,会回到 STATE_ACTIVE 状态,当下次满足灭屏不充电时,持续的时间就由 30mins 缩短为 10mins!
1 | MOTION_INACTIVE_TIMEOUT = mParser.getLong(KEY_MOTION_INACTIVE_TIMEOUT, |
- IDLE_AFTER_INACTIVE_TIMEOUT
deep idle 时间属性,取值为 30 mins,当设备在 STATE_INACTIVE 阶段是会监听设备运动,需要保持不运动 30mins 才能进入 STATE_IDLE_PENDING 阶段!!
1 | long idleAfterInactiveTimeout = (mHasWatch ? 15 : 30) * 60 * 1000L; |
- IDLE_PENDING_TIMEOUT
deep idle 时间属性,取值为 5 mins,在 deep idle 状态下的初始时间窗!
1 | IDLE_PENDING_TIMEOUT = mParser.getLong(KEY_IDLE_PENDING_TIMEOUT, |
MAX_IDLE_PENDING_TIMEOUT
deep idle 时间属性,取值为 10 mins,在 deep idle 状态下的最大时间窗!1
2MAX_IDLE_PENDING_TIMEOUT = mParser.getLong(KEY_MAX_IDLE_PENDING_TIMEOUT,
!COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L);IDLE_PENDING_FACTOR
deep idle 因子属性,取值为 2f,用于调整 deep idle 的时间窗!
1 | IDLE_PENDING_FACTOR = mParser.getFloat(KEY_IDLE_PENDING_FACTOR, |
IDLE_TIMEOUT
deep idle 时间属性,取值为 60 mins,deep idle 状态的初始持续时间!1
2IDLE_TIMEOUT = mParser.getLong(KEY_IDLE_TIMEOUT,
!COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L);MAX_IDLE_TIMEOUT
deep idle 时间属性,取值为 6 hours,deep idle 状态的最大持续时间!1
2MAX_IDLE_TIMEOUT = mParser.getLong(KEY_MAX_IDLE_TIMEOUT,
!COMPRESS_TIME ? 6 * 60 * 60 * 1000L : 30 * 60 * 1000L);IDLE_FACTOR
deep idle 因子属性,取值为 2f,用于调整 deep idle 的时间窗!1
IDLE_FACTOR = mParser.getFloat(KEY_IDLE_FACTOR, 2f);
MIN_TIME_TO_ALARM
deep idle 时间属性,取值为 60mins,在每次改变 deep 状态时,会判断是否有 Alarm 在该时间内触发,如果有,不会进入 deep idle 状态!
1 | MIN_TIME_TO_ALARM = mParser.getLong(KEY_MIN_TIME_TO_ALARM, |
2.1.2.3 其他相关变量
MAX_TEMP_APP_WHITELIST_DURATION
doze 模式的临时缓存白名单的最大有效时间,取值为 5mins!1
2MAX_TEMP_APP_WHITELIST_DURATION = mParser.getLong(
KEY_MAX_TEMP_APP_WHITELIST_DURATION, 5 * 60 * 1000L);MMS_TEMP_APP_WHITELIST_DURATION
MMS 应用的 doze 模式的临时缓存白名单的有效时间,60s
1 | MMS_TEMP_APP_WHITELIST_DURATION = mParser.getLong( |
SMS_TEMP_APP_WHITELIST_DURATION
SMS 应用的 doze 模式的临时缓存白名单的有效时间,20s1
2SMS_TEMP_APP_WHITELIST_DURATION = mParser.getLong(
KEY_SMS_TEMP_APP_WHITELIST_DURATION, 20 * 1000L);NOTIFICATION_WHITELIST_DURATION
通知的白名单时间间隔,30s
1 | NOTIFICATION_WHITELIST_DURATION = mParser.getLong( |
对于这些变量的使用,我们后续再分析!
2.2 DeviceIdleController.readConfigFileLocked
1 | void readConfigFileLocked() { |
继续调用另一个 readConfigFileLocked 方法!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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55private void readConfigFileLocked(XmlPullParser parser) {
final PackageManager pm = getContext().getPackageManager();
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
;
}
if (type != XmlPullParser.START_TAG) {
throw new IllegalStateException("no start tag found");
}
int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("wl")) {
String name = parser.getAttributeValue(null, "n");
if (name != null) {
try {
ApplicationInfo ai = pm.getApplicationInfo(name,
PackageManager.MATCH_UNINSTALLED_PACKAGES);
//【1】将用户应用程序的包名和 appId 加入到 mPowerSaveWhitelistUserApps 中;
mPowerSaveWhitelistUserApps.put(ai.packageName,
UserHandle.getAppId(ai.uid));
} catch (PackageManager.NameNotFoundException e) {
}
}
} else {
Slog.w(TAG, "Unknown element under <config>: "
+ parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
} catch (IllegalStateException e) {
Slog.w(TAG, "Failed parsing config " + e);
} catch (NullPointerException e) {
Slog.w(TAG, "Failed parsing config " + e);
} catch (NumberFormatException e) {
Slog.w(TAG, "Failed parsing config " + e);
} catch (XmlPullParserException e) {
Slog.w(TAG, "Failed parsing config " + e);
} catch (IOException e) {
Slog.w(TAG, "Failed parsing config " + e);
} catch (IndexOutOfBoundsException e) {
Slog.w(TAG, "Failed parsing config " + e);
}
}
可以看到,readConfigFileLocked 方法最终解析 /data/system/deviceidle.xml 文件。
该文件中保存的是在 device mode 模式下,能够后台运行的用户应用白名单!
最终的解析结果,保存到了 mPowerSaveWhitelistUserApps 集合中,packageName 和 appId 的映射!
2.3 DeviceIdleController.updateWhitelistAppIdsLocked
我们先来回顾下前面涉及到的几个集合:
mPowerSaveWhitelistAppsExceptIdle:保存在 power save 模式下可以后台运行,但在 doze 模式下不能后台运行的系统应用名单,packageName 和 appId 的映射!
mPowerSaveWhitelistSystemAppIdsExceptIdle:保存在 power save 模式下可以后台运行,但在 doze 模式下不能后台运行的系统应用名单,appId 和 true 的映射!
mPowerSaveWhitelistApps:保存在 power save 模式下可以后台运行的系统应用,packageName 和 appId 的映射!
mPowerSaveWhitelistSystemAppIds:保存在 power save 模式下可以后台运行的系统应用,appId 和 true 的映射!
1 | private void updateWhitelistAppIdsLocked() { |
1、这里又设计到了几个重要集合:
- mPowerSaveWhitelistExceptIdleAppIds:
保存在 power save 模式下可以后台运行,但在 doze 模式下不能后台运行的系统和用户应用,packageName 和 appId 的映射;
集合来自 mPowerSaveWhitelistAppsExceptIdle 和 mPowerSaveWhitelistUserApps 的合并;
- mPowerSaveWhitelistExceptIdleAppIdArray:
保存在 power save 模式下可以后台运行,但在 doze 模式下不能后台运行的系统和用户应用 appId;
集合来自 mPowerSaveWhitelistAppsExceptIdle 和 mPowerSaveWhitelistUserApps 的合并;
- mPowerSaveWhitelistAllAppIds:
保存在 power save 模式下可以后台运行的系统和用户应用 (appId 和 true 的映射);
集合来自 mPowerSaveWhitelistApps 和 mPowerSaveWhitelistUserApps 的合并;
- mPowerSaveWhitelistAllAppIdArray:
保存在 power save 模式下可以后台运行的系统和用户应用 appId;
集合来自 mPowerSaveWhitelistApps 和 mPowerSaveWhitelistUserApps 的合并;
- mPowerSaveWhitelistUserAppIds:
保存在 power save 模式下可以后台运行的用户应用 (appId 和 true 的映射);
集合来自 mPowerSaveWhitelistUserApps;
- mPowerSaveWhitelistUserAppIdArray:
保存在 power save 模式下可以后台运行的用户应用 appId;
集合来自 mPowerSaveWhitelistUserApps;
2、传递名单
接着,将 mPowerSaveWhitelistAllAppIdArray 传递给了 PowerManagerService;
然后,将 mPowerSaveWhitelistUserAppIdArray 传递给了 AlarmManagerService;
2.3.1 DeviceIdleController.buildAppIdArray
1 | private static int[] buildAppIdArray(ArrayMap<String, Integer> systemApps, |
2.4 new BinderService
创建了一个 BinderService 对象,继承了 IDeviceIdleController.Stub,作为服务端桩对象,用于其它进程访问 DeviceIdleController 内部接口!!
1 | private final class BinderService extends IDeviceIdleController.Stub { |
这里的接口,我们先不详细看!
最后,将桩对象注册到了 ServiceManager 中!
1 | publishBinderService(Context.DEVICE_IDLE_CONTROLLER, mBinderService); |
2.5 new LocalService
创建了本地服务对象,方便进程内部通信!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
27public final class LocalService {
public void addPowerSaveTempWhitelistAppDirect(int appId, long duration, boolean sync,
String reason) {
addPowerSaveTempWhitelistAppDirectInternal(0, appId, duration, sync, reason);
}
public long getNotificationWhitelistDuration() {
return mConstants.NOTIFICATION_WHITELIST_DURATION;
}
public void setNetworkPolicyTempWhitelistCallback(Runnable callback) {
setNetworkPolicyTempWhitelistCallbackInternal(callback);
}
public void setJobsActive(boolean active) {
DeviceIdleController.this.setJobsActive(active);
}
// Up-call from alarm manager.
public void setAlarmsActive(boolean active) {
DeviceIdleController.this.setAlarmsActive(active);
}
public int[] getPowerSaveWhitelistUserAppIds() {
return DeviceIdleController.this.getPowerSaveWhitelistUserAppIds();
}
}
3 DeviceIdleController.onBootPhase
1 |
|
这里我们来看看 DisplayListener 对象!
3.1 new BroadcastReceiver
1 | private final BroadcastReceiver mReceiver = new BroadcastReceiver() { |
创建了一个 BroadcastReceiver 对象,监听 CONNECTIVITY_ACTION,ACTION_BATTERY_CHANGED,ACTION_PACKAGE_REMOVED 广播!
关于监听到变化后的动态处理,这里我们先不分析!
3.1.1 DeviceIdleController.removePowerSaveWhitelistAppInternal
1 | public boolean removePowerSaveWhitelistAppInternal(String name) { |
reportPowerSaveWhitelistChangedLocked 方法会发送 PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED 广播!1
2
3
4
5private void reportPowerSaveWhitelistChangedLocked() {
Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
}
writeConfigFileLocked 方法会发送 MSG_WRITE_CONFIG 消息给 MyHandler。MyHandler 会处理消息,调用 handleWriteConfigFile -> writeConfigFileLocked(XmlSerializer out) 方法,更新本地名单!1
2
3
4void writeConfigFileLocked() {
mHandler.removeMessages(MSG_WRITE_CONFIG);
mHandler.sendEmptyMessageDelayed(MSG_WRITE_CONFIG, 5000);
}
继续看!
3.2 register DisplayListener
mDisplayListener 用于监听屏幕的状态!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private final DisplayManager.DisplayListener mDisplayListener
= new DisplayManager.DisplayListener() {
public void onDisplayAdded(int displayId) {
}
public void onDisplayRemoved(int displayId) {
}
public void onDisplayChanged(int displayId) {
if (displayId == Display.DEFAULT_DISPLAY) {
synchronized (DeviceIdleController.this) {
//【3.1.1】当屏幕状态发生变化后,onDisplayChanged 方法被触发!
// 执行了 DeviceIdleController.updateDisplayLocked 方法!
updateDisplayLocked();
}
}
}
};
3.2.1 DeviceIdleController.updateDisplayLocked
updateDisplayLocked 用于更新屏幕的状态!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void updateDisplayLocked() {
//【1】获取屏幕当前的状态,并判断是否是亮屏状态!
mCurDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
boolean screenOn = mCurDisplay.getState() == Display.STATE_ON;
if (DEBUG) Slog.d(TAG, "updateDisplayLocked: screenOn=" + screenOn);
if (!screenOn && mScreenOn) {
//【2.1】如果是从亮屏转为熄屏,设置 mScreenOn 为 false!
mScreenOn = false;
if (!mForceIdle) {
becomeInactiveIfAppropriateLocked(); // 进入 Doze 模式;
}
} else if (screenOn) {
//【2.2】如果是从熄屏转亮屏,设置 mScreenOn 为 true!
mScreenOn = true;
if (!mForceIdle) {
becomeActiveLocked("screen", Process.myUid()); // 退出 Doze 模式;
}
}
}
mForceIdle 表示是否强制进入 idle 状态,默认为 false 的,目前唯一的开启方式是通过 adb shell,执行 dumpsys 命令,触发 force-idle,force-inactive 相关指令,强制进入 idle 状态!!
这里看到,当熄屏后,会调用 becomeInactiveIfAppropriateLocked 方法,进入 doze 模式;当亮屏后,会调用 becomeActiveLocked 方法,退出 doze!
3.3 DeviceIdleController.updateConnectivityState
更新网络状态!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
37
38
39void updateConnectivityState(Intent connIntent) {
ConnectivityService cm;
synchronized (this) {
cm = mConnectivityService;
}
if (cm == null) {
return;
}
NetworkInfo ni = cm.getActiveNetworkInfo();
synchronized (this) {
boolean conn;
if (ni == null) {
//【1】网络断开,conn 为 false;
conn = false;
} else {
//【2】获得网络的连接状态;
if (connIntent == null) {
conn = ni.isConnected();
} else {
final int networkType =
connIntent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE,
ConnectivityManager.TYPE_NONE);
if (ni.getType() != networkType) {
return;
}
conn = !connIntent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY,
false);
}
}
//【3】处理连接状态,如果当前状态和之前的状态发生了变化,更新 mNetworkConnected 的值
if (conn != mNetworkConnected) {
mNetworkConnected = conn;
//【4】如果本次状态是处于连接中,并且 light idle 正在等待网络,那就继续处理状态!!
if (conn && mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
stepLightIdleStateLocked("network");
}
}
}
}
可以看到 DeviceIdleController 的启动流程还是很简单的!
4 启动总结
我们通过一张图来看看 DeviceIdleController 的整个启动过程!
其他优秀博客!!
https://blog.csdn.net/kc58236582/article/details/54923406
https://www.cnblogs.com/gavanwanggw/p/7327832.html