深入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()

这个方法才是真正干活的。我把它拆成三个步骤来讲:

  1. 判断是否应该进入Doze
  2. 计算Doze的超时时间
  3. 启动或停止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模式超时机制流程图 T0 T1 (30min) T2 (60min) T3 (90min) T4 (120min) 空闲 轻度 中度 深度 静止 维护窗口(Maintenance Window) 每次超时后,系统会短暂唤醒,处理积压的网络请求和闹钟 超时时间计算公式: nextTimeout = now + (mDozeTimeout * (1 + mDozeCount)) 其中 mDozeTimeout 初始值为 30分钟,每次递增 注:实际时间可能因厂商定制而不同,但逻辑框架一致

这张图展示了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状态。我经常用这个命令来验证我的修改是否生效。输出里会显示mDozeModeActivemDozeTimeout等关键字段。

总结

PMS中Doze的Java层实现,说白了就是一套"条件判断+定时器+状态通知"的机制。它不复杂,但细节很多。我个人觉得,理解Doze的关键在于把握住三个核心:

  1. 进入条件:屏幕关、无活动、设备静、无锁
  2. 超时机制:渐进式超时,每次递增
  3. 维护窗口:短暂的唤醒机会,处理紧急任务

掌握了这三点,你就掌握了Doze的骨架。剩下的血肉,比如具体的超时计算、传感器交互、电源锁管理,都是在这些骨架上长出来的。

好了,这一章就到这里。下一章我们会深入Native层,看看Doze模式在底层是怎么和内核交互的。到时候你会发现,Java层只是冰山一角,真正的"省电魔法"其实藏在C++代码里。


公众号:蓝海资料掘金营,微信deep3321