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会集中处理。我画了一张图,帮你理解这个流程:
当系统进入维护窗口时,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问题了。