29. 源码分析:AlarmManagerService如何处理Doze下的Alarm

各位同学,今天我们来啃一块硬骨头——AlarmManagerService在Doze模式下是怎么处理Alarm的。说实话,这部分源码我前前后后看了不下五遍,每次都有新发现。你想想看,Doze模式把系统搞得半死不活的,Alarm还得准时响,这中间的门道可不少。

入口:AlarmManagerService的set方法

我们先从最常用的set()方法说起。应用层调用AlarmManager.set(),最终会走到AlarmManagerService.setImpl()。我个人习惯把这里叫做“Alarm的登记处”。

// AlarmManagerService.java
private void setImpl(int type, long triggerAtTime, long windowLength,
        long interval, PendingIntent operation, WorkSource workSource,
        AlarmClockInfo alarmClock) {
    // 1. 解析参数,创建Alarm对象
    Alarm a = new Alarm(type, triggerAtTime, windowLength, interval,
            operation, workSource, alarmClock);
    
    // 2. 加锁,防止并发
    synchronized (mLock) {
        // 3. 核心逻辑:根据Doze状态决定Alarm的去向
        if (mPendingIdleUntil != null) {
            // 系统正在Doze中...
            deliverAlarmToDoze(a);
        } else {
            // 正常模式,走常规流程
            deliverAlarmToNormal(a);
        }
    }
}

嗯,这里要注意一个关键变量——mPendingIdleUntil。它就像一个“Doze开关”,只要它不为null,说明系统正在Doze模式里待着呢。

Doze下的Alarm分类:两种命运

在Doze模式下,Alarm会被分成两类。我当年第一次看这段代码时,差点被绕晕。其实说白了,就两种命运:

Alarm类型 标志位 Doze下的行为
常规Alarm 无特殊标志 被推迟,等到Doze维护窗口才触发
白名单Alarm FLAG_ALLOW_WHILE_IDLE 允许在Doze下触发,但有频率限制

为什么会这样设计?你想想看,如果所有Alarm都在Doze下照常触发,那Doze模式就形同虚设了。但有些Alarm又必须准时,比如闹钟、消息推送的保活心跳。所以Google搞了个折中方案。

核心逻辑:AlarmManagerService通过Alarm.isDozeWhitelisted()方法判断Alarm是否属于白名单。这个方法检查的是flags字段中是否设置了FLAG_ALLOW_WHILE_IDLE

deliverAlarmToDoze:白名单Alarm的处理

我们来看看deliverAlarmToDoze这个方法。我记得有一次在项目中排查一个Alarm不准时的问题,就是在这里找到了线索。

private void deliverAlarmToDoze(Alarm a) {
    if (a.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) {
        // 白名单Alarm,放入mAlarmBatches
        // 但会限制频率:每个应用每9秒最多一个
        if (checkAllowWhileIdleQuota(a)) {
            mAlarmBatches.add(new Batch(a));
            rescheduleKernelAlarmsLocked();
        } else {
            // 超限了,推迟到下一个维护窗口
            mPendingWhileIdleAlarms.add(a);
        }
    } else {
        // 非白名单Alarm,直接推迟
        mPendingWhileIdleAlarms.add(a);
    }
}

这里有个坑,我曾经踩过——checkAllowWhileIdleQuota这个配额检查。它限制每个应用在Doze下每9秒最多只能触发一个白名单Alarm。为什么是9秒?我猜Google做过大量测试,这个间隔既能保证基本功能,又不会让系统频繁唤醒。

避坑指南:如果你在开发中遇到Doze下Alarm不触发的问题,先检查两件事:1) 是否设置了FLAG_ALLOW_WHILE_IDLE;2) 是否触发了9秒配额限制。我曾经有个项目,就是因为心跳包太频繁被限流了,导致消息推送延迟。

Doze维护窗口:Alarm的“放风时间”

Doze模式下,系统会定期进入维护窗口(maintenance window)。这时候,所有被推迟的Alarm会集中处理。我画了一张图,帮你理解这个流程:

Doze模式下Alarm处理流程 Doze深度睡眠 维护窗口 再次进入Doze Alarm被推迟: mPendingWhileIdleAlarms 队列中等待 白名单Alarm每9秒限一个,超限的也进入等待队列 维护窗口触发: AlarmManagerService.onIdleMaintenance() 被调用 系统会唤醒CPU,处理所有待处理的Alarm 关键源码路径: 1. AlarmManagerService.setImpl() → 判断Doze状态 2. deliverAlarmToDoze() → 分类处理Alarm 3. onIdleMaintenance() → 维护窗口触发Alarm 4. rescheduleKernelAlarmsLocked() → 重新设置内核Alarm

当系统进入维护窗口时,onIdleMaintenance()方法会被调用。它会遍历mPendingWhileIdleAlarms队列,把里面的Alarm全部拿出来执行。

void onIdleMaintenance() {
    synchronized (mLock) {
        // 取出所有被推迟的Alarm
        ArrayList<Alarm> alarms = mPendingWhileIdleAlarms;
        mPendingWhileIdleAlarms = new ArrayList<>();
        
        // 重新调度这些Alarm
        for (Alarm a : alarms) {
            // 设置触发时间为当前时间
            a.triggerTime = System.currentTimeMillis();
            mAlarmBatches.add(new Batch(a));
        }
        
        // 更新内核Alarm
        rescheduleKernelAlarmsLocked();
    }
}

个人经验:我在做低功耗优化时,经常用dumpsys alarm命令查看Alarm的排队情况。如果发现mPendingWhileIdleAlarms队列很长,说明有很多Alarm被推迟了。这时候就要检查一下,是不是有应用没有正确使用白名单标志。

内核层的Alarm设置

最后,AlarmManagerService会把Alarm设置到内核层。这里用的是rescheduleKernelAlarmsLocked()方法。它会找到最近要触发的Alarm,然后通过JNI调用内核的set_alarm()函数。

嗯,这里要注意,内核层的Alarm是系统唤醒的关键。如果内核Alarm设置错了,整个Doze模式的时间管理就乱套了。我遇到过一个问题,就是内核Alarm被频繁重置,导致系统无法进入深度睡眠。后来发现是某个应用在短时间内反复设置Alarm,把内核Alarm的时间戳改来改去。

好了,关于AlarmManagerService在Doze下的处理逻辑,我们就分析到这里。核心就是记住三点:白名单Alarm有特权但有限频、非白名单Alarm被推迟到维护窗口、内核Alarm是最终的执行者。搞懂这些,你就能在项目中游刃有余地处理Doze下的Alarm问题了。

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