深入PowerManagerService:Doze模式的Java层源码解析
各位同学,今天我们来啃一块硬骨头——PowerManagerService(以下简称PMS)中Doze模式的Java层实现。说实话,这部分代码我前前后后看了不下十遍,每次都有新收获。它就像一座冰山,表面上看只是几个状态切换,底下却藏着复杂的定时器、传感器监听和电源锁管理。
Doze模式的入口:从哪里开始?
我们先找到入口。PMS里处理Doze的核心方法是updatePowerStateLocked()。嗯,这个名字很朴实,但它的工作量可不小。
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void updatePowerStateLocked() {
if (!mSystemReady || mDirty == 0) {
return;
}
// ... 其他状态更新 ...
updateDozeModeLocked(); // 这里就是Doze的入口
// ...
}
我个人习惯在分析这类代码时,先找mDirty这个标志位。它就像个"待办事项清单",告诉PMS哪些状态需要重新计算。当系统进入空闲状态、屏幕关闭、或者设备静止时,mDirty就会被标记上Doze相关的位。
核心方法:updateDozeModeLocked()
这个方法才是真正干活的。我把它拆成三个步骤来讲:
- 判断是否应该进入Doze
- 计算Doze的超时时间
- 启动或停止Doze定时器
private void updateDozeModeLocked() {
// 步骤1:判断条件
boolean shouldDoze = shouldEnterDozeModeLocked();
if (shouldDoze && !mDozeModeActive) {
// 步骤2:计算超时
long now = SystemClock.uptimeMillis();
long nextTimeout = computeDozeTimeoutLocked(now);
// 步骤3:启动定时器
scheduleDozeTimeoutLocked(nextTimeout);
mDozeModeActive = true;
// 通知其他组件
notifyDozeModeChangedLocked(true);
} else if (!shouldDoze && mDozeModeActive) {
// 退出Doze
cancelDozeTimeoutLocked();
mDozeModeActive = false;
notifyDozeModeChangedLocked(false);
}
}
这里有个细节我踩过坑:shouldEnterDozeModeLocked()的判断条件非常严格。我曾经在项目里发现设备明明空闲了半小时,却始终不进Doze。后来一查,原来是某个传感器一直在上报数据,导致mDirty被持续置位。
判断条件:到底什么才算"空闲"?
我们来看看shouldEnterDozeModeLocked()里到底检查了什么:
private boolean shouldEnterDozeModeLocked() {
// 1. 屏幕必须关闭
if (mWakefulness != WAKEFULNESS_ASLEEP) {
return false;
}
// 2. 没有正在进行的用户活动
if (mUserActivityTimeoutOverrideMs > 0) {
return false;
}
// 3. 设备必须静止(通过传感器判断)
if (!mDeviceIdleModeEnabled) {
return false;
}
// 4. 没有持有PARTIAL_WAKE_LOCK的应用
if (hasWakeLocksLocked()) {
return false;
}
return true;
}
你看,一共四个条件,缺一不可。说白了,Doze模式就是"屏幕关着、没人碰、设备不动、没应用在干活"这四件事同时成立时才会触发。
关键点:mDeviceIdleModeEnabled这个标志位是由DeviceIdleController设置的。PMS和DeviceIdleController之间通过回调通信,这也是Android系统模块化设计的一个典型例子。
超时计算:Doze的"倒计时"机制
进入Doze不是瞬间完成的,它有一个渐进的超时机制。我画了一张图来展示这个过程:
这张图展示了Doze的渐进式超时机制。每次超时后,系统会进入一个"维护窗口",让应用有机会处理一些紧急任务。维护窗口的时间很短,大概只有几秒钟。
定时器的实现:AlarmManager的妙用
PMS里Doze的定时器不是自己实现的,而是借用了AlarmManager。为什么?因为AlarmManager本身就是系统级的定时服务,而且它知道怎么处理省电场景。
private void scheduleDozeTimeoutLocked(long timeout) {
// 取消之前的定时器
cancelDozeTimeoutLocked();
// 使用AlarmManager设置一次性闹钟
AlarmManager alarms = mAlarmManager;
PendingIntent pi = PendingIntent.getBroadcast(
mContext, 0, new Intent(ACTION_DOZE_TIMEOUT),
PendingIntent.FLAG_IMMUTABLE);
alarms.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
timeout, pi);
}
这里有个坑:setExact方法在Android 12之后有了行为变化。我记得有一次升级系统后,Doze的定时器不准了,排查了半天才发现是AlarmManager的精确闹钟策略改了。后来我改用了setAlarmClock才解决问题。
注意事项:在Android 12+上,setExact的精度会受到省电策略影响。如果你在定制系统时发现Doze行为异常,优先检查AlarmManager的版本兼容性。
状态通知:Doze模式如何影响其他模块?
当Doze模式切换时,PMS会通过notifyDozeModeChangedLocked()通知其他组件。这个通知链很有意思:
private void notifyDozeModeChangedLocked(boolean active) {
// 1. 通知DisplayManagerService
mDisplayManagerInternal.setDozeMode(active);
// 2. 通知WindowManagerService
mWindowManagerInternal.setDozeMode(active);
// 3. 通知BatteryStatsService(用于统计)
mBatteryStatsInternal.noteDozeMode(active);
// 4. 通过Handler发送广播
Message msg = mHandler.obtainMessage(MSG_DOZE_MODE_CHANGED,
active ? 1 : 0, 0);
mHandler.sendMessage(msg);
}
你看,一个Doze模式的切换,牵动了显示、窗口、电池统计等多个模块。这就是Android系统设计的精妙之处——通过事件驱动,让各个模块各司其职。
避坑指南:我踩过的三个雷
做Doze相关开发这几年,我总结了几条血泪教训:
- 不要依赖Doze的精确时间:不同厂商的定制ROM可能会修改超时参数,你的代码应该能适应任何超时时间。
- 注意唤醒锁的持有时间:我曾经在维护窗口里持有一个唤醒锁超过30秒,结果被系统强制回收,导致数据丢失。
- 测试时记得关闭开发者选项:开发者选项里的"不保留活动"和"后台进程限制"会影响Doze的行为,测试结果可能不准确。
调试技巧:用adb shell dumpsys power可以查看当前Doze状态。我经常用这个命令来验证我的修改是否生效。输出里会显示mDozeModeActive、mDozeTimeout等关键字段。
总结
PMS中Doze的Java层实现,说白了就是一套"条件判断+定时器+状态通知"的机制。它不复杂,但细节很多。我个人觉得,理解Doze的关键在于把握住三个核心:
- 进入条件:屏幕关、无活动、设备静、无锁
- 超时机制:渐进式超时,每次递增
- 维护窗口:短暂的唤醒机会,处理紧急任务
掌握了这三点,你就掌握了Doze的骨架。剩下的血肉,比如具体的超时计算、传感器交互、电源锁管理,都是在这些骨架上长出来的。
好了,这一章就到这里。下一章我们会深入Native层,看看Doze模式在底层是怎么和内核交互的。到时候你会发现,Java层只是冰山一角,真正的"省电魔法"其实藏在C++代码里。