最近项目上不忙,用AI来夯实基础,其中就聊到广播的超时。看到的很多技术资料都说,前台广播10s超时,后台60s超时。具体的逻辑也没有去看,但是大家约定俗成的认为不能在onReceive()方法中执行耗时操作,所以一般也很少遇到广播导致的超时。今天跟AI对话了一下,才发现对于广播超时这一块有很大的误解。根据多轮聊下来的结果,然后整理记录了这篇文章。虽然感觉可能用不上,但是在AI发展的越来越快的当下,技术的深度和经验是保持不容易淘汰和替代的防火墙吧(个人感觉)。继续夯实基础吧~
回到文章主题。
BroadcastReceiver的onReceive()中执行耗时操作一定会导致anr吗?
答案是:不一定,需要先看这次投递是否接受超时检查。
onReceive() 执行耗时操作,不一定触发本次广播的超时 ANR。 普通 Context.sendBroadcast() 投递给动态注册接收者时,会满足 assumeDelivered = true 1.1章节解释:系统成功调度后就将这次投递视为完成,不等待实际处理结束,也不会启动这次投递的广播超时检查。1 2 3
// BroadcastQueueModernImpl.java
// 系统成功调度后就将这次投递视为完成,不等待实际处理结束,也不会启动这次投递的广播超时检查
/**
* A receiver is about to be dispatched. Start ANR timers, if necessary.
*
* @return {@code true} if this a blocking delivery. That is, we are going to block on the
* finishReceiver() to be called before moving to the next broadcast. Otherwise,
* {@code false}.
*/
@GuardedBy("mService")
@CheckResult
private boolean dispatchReceivers(@NonNull BroadcastProcessQueue queue,
@NonNull BroadcastRecord r, int index) throws BroadcastRetryException {
final ProcessRecord app = queue.app;
final Object receiver = r.receivers.get(index);
// Skip ANR tracking early during boot, when requested, or when we
// immediately assume delivery success
final boolean assumeDelivered = r.isAssumedDelivered(index);
// 判断条件,如果assumeDelivered = true则启动超时判定的分支走不进去
if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) {
queue.setTimeoutScheduled(true);
// 此处就是根据前台还是后台广播来判断超时的时间,前台10s,后台60s
final int softTimeoutMillis = (int) (r.isForeground() ? mFgConstants.TIMEOUT
: mBgConstants.TIMEOUT);
// 开启超时
startDeliveryTimeoutLocked(queue, softTimeoutMillis);
} else {
queue.setTimeoutScheduled(false);
}
...
}
但不能反过来概括成“无序广播都没有 ANR 超时限制”。同样是无序广播,投递给 Manifest注册的接收者,或通过系统内部接口携带非空的framework完成回调,都不满足上述条件。1 4
常见场景可以直接对照下表。这里判断的是某个接收者的一次投递,不是整个 Intent,也不是接收者对象永久具有的属性。1
| 投递场景 | ordered | resultTo | assumeDelivered | 本次广播处理超时检查 |
|---|---|---|---|---|
普通 sendBroadcast() → 动态注册的接收者 | false | null | true | 不启动 |
普通 sendBroadcast() → Manifest 注册的接收者 | false | null | false | 按条件启动 |
| 有序广播 → 链中的业务接收者 | true | 可以为空,也可以非空 | false | 按条件启动 |
| 系统内部无序广播+非空完成回调 → 动态接收者 | false | 非空 | false | 按条件启动 |
表中结论由 BroadcastRecord.isAssumedDelivered() 与BroadcastQueueModernImpl.dispatchReceivers() 共同决定。“按条件启动”还要求系统已就绪、广播没有超时豁免,并且进入实际投递路径;assumeDelivered = false 不是“必然发生 ANR”的充分条件。1 2
本文所有“不启动”的结论,都只针对本次广播的处理超时检查,不等于应用不会发生其他 ANR,也不是无限执行耗时任务的保障。 2 8 9
1 核心证据:超时检查由什么决定?
1.1 是否“假定已投递”?
BroadcastRecord.isAssumedDelivered() 的实现如下:1
boolean isAssumedDelivered(int index) {
return (receivers.get(index) instanceof BroadcastFilter) && !ordered
&& (resultTo == null);
}
必须同时满足三个条件:接收者是动态注册的、广播是无序的、没有framework最终回调。动态接收者在广播记录中对应 BroadcastFilter,Manifest 接收者对应 ResolveInfo,因此后者不满足第一个条件1。
assumeDelivered 的含义是“framework可以不等待应用的完成回执”,而不是“业务逻辑已经执行成功”。2
1.2 是否启动广播超时计时器?
BroadcastQueueModernImpl.dispatchReceivers() 中的关键条件如下。保留原判断,省略计时器内部实现:2
final boolean assumeDelivered = r.isAssumedDelivered(index);
if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) {
// 启动本次投递的广播超时检查,内部实现省略。
}
因此,assumeDelivered = true 会直接排除这次计时。它不是“允许执行更长时间”,而是没有启动这里的广播处理超时检查。2
动态接收者成功调度后的代码进一步印证这一点:2
if (assumeDelivered) {
finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_DELIVERED,
"assuming delivered");
return false;
}
这一步完成的是系统队列的状态记账,不是确认 onReceive() 已经执行结束。2
对于启动了检查的投递,若迟迟没有完成,系统可以将其置为 DELIVERY_TIMEOUT,再进入 appNotResponding() 路径;源码还保留了应用调试状态等额外判断。因此,“启动计时”与“最终报告 ANR”也不是同一件事。2
1.3 应用侧证据:完成回执也受同一个标志控制
BroadcastReceiver.PendingResult.sendFinished() 中有以下分支:6
if (!mAssumeDeliveredHint) {
if (mOrderedHint) {
am.finishReceiver(mToken, mResultCode, mResultData, mResultExtras,
mAbortBroadcast, mFlags);
} else {
am.finishReceiver(mToken, 0, null, null, false, mFlags);
}
}
mAssumeDeliveredHint = true 时,不向 AMS 发送这个完成回执。为 false 时才回报;其中,无序分支虽然不携带业务结果,仍然可以报告“本次接收已结束”。6
服务端的“不等待、不计时”和应用侧的“不发送完成回执”,由此形成一致的处理协议。2 6
2 有序广播与普通无序广播,为什么结论不同?
2.1 普通无序广播:发送方式只确定两个条件
普通发送方式:
context.sendBroadcast(intent)
ContextImpl 向 AMS 提交时,不提供最终结果接收方,并将有序标志设为 false。对应到广播记录就是:3
ordered = false
resultTo = null
投递给动态接收者时,三个条件全部满足,得到 assumeDelivered = true;投递给 Manifest 接收者时,第一个条件不成立,得到 false。1
所以,“普通 sendBroadcast()+动态接收者”才是完整结论,不能省略接收者类型。 即使是同一次发送,对不同接收者也可能有不同的超时处理方式。1 2
2.2 有序广播:不需要最终结果,也仍需等待链中接收者完成
例如:
context.sendOrderedBroadcast(intent, null)
这里的 null 是 receiverPermission,这个重载没有提供最终结果回调,但仍设置了 ordered = true。因此,即使 resultTo == null,!ordered 也已是 false,链中业务接收者不会进入“假定已投递”路径。1 3
有序广播需要根据当前接收者的完成状态推进后续投递。满足计时条件后,处理超过预算且未完成,可能触发广播超时 ANR。是否向发送方返回业务结果,不是唯一决定因素。 2
使用带 resultReceiver 参数的有序广播重载,可以接收通过 setResult() 等接口传递的最终业务结果;但不是传入这个参数之后,有序广播才开始需要完成确认。3 7
2.3 最终结果回调本身,是另一条投递路径
不要把广播链中处理业务的接收者与发送时传入的最终 resultReceiver混为一谈。BroadcastQueueModernImpl.scheduleResultTo() 向应用调度最终结果回调时,直接使用 assumeDelivered = true。2
因此,有序广播链中的业务接收者按条件接受超时检查,不代表最终结果回调的 onReceive() 也使用相同的计时规则。该独立路径不套用前面的业务接收者判定表。2
3 无序广播不支持返回结果,为什么还要判断 resultTo == null?
这是最容易产生误解的地方:不支持有序广播那套业务结果机制,不等于framework内部一定没有完成回调。 4 7
3.1 业务结果、完成回执、最终回调,是三个概念
| 概念 | 解决的问题 |
|---|---|
业务结果:setResult() 等 | 当前接收者输出了什么结果码、描述和数据?普通无序广播不支持这套结果传递机制。 |
单次完成回执:finishReceiver() | 当前这个接收者的本次广播处理是否结束?不一定携带业务数据。 |
framework最终回调:resultTo | 广播整体按framework规则收尾时,要通知谁?可以用于结果通知,也可以只用于完成通知。 |
这些区别分别体现在公开 API、PendingResult.sendFinished() 和系统内部的完成回调接口中。4 6 7
因此,!ordered 排除的是有序广播链的等待需求;resultTo == null 排除的是framework最终完成通知的等待需求。它们不是重复条件。1 4
3.2 实际源码:无序广播可以带非空完成回调
系统内部接口 ActivityManagerInternal.broadcastIntentWithCallback() 支持“不保证接收顺序,但提供完成回调”。它不是普通应用的 Context.sendBroadcast() 公开接口。其 AMS 实现保留了 resultTo,同时明确传入 serialized = false:4 4a
return broadcastIntent(intent, resultTo, requiredPermissions, false /* serialized */,
userId, appIdAllowList, filterExtrasForReceiver, bOptions);
serialized 最终赋给 BroadcastRecord.ordered,所以可以出现:1 4
ordered = false
resultTo != null
真实例子是电源管理 Notifier 的亮屏、灭屏通知。 其中亮屏广播通过以下调用携带非空完成回调,灭屏广播也走类似路径:5
mActivityManagerInternal.broadcastIntentWithCallback(mScreenOnIntent,
mWakeUpBroadcastDone, null, UserHandle.USER_ALL,
null, null, mScreenOnOffOptions);
mWakeUpBroadcastDone 收到完成通知后会调用 sendNextBroadcast(),推进后续流程。对这类广播,即使当前业务接收者是动态注册的,也会因 resultTo != null 而得到 assumeDelivered = false。1 5
由此可见,单凭 isOrderedBroadcast() == false,不足以判断没有广播超时检查。1 2
3.3 省略第二个条件,会破坏什么?
假设将判断改成“动态接收者且无序”,带完成回调的无序投递也会被提前标记为完成。结合 dispatchReceivers() 的处理可以推导:framework可能在接收者实际尚未完成时,就推进最终回调,破坏原本需要的完成等待关系。 这就是 resultTo == null 不能省略的原因。1 2 4
这里的“完成通知”指framework按规则收尾,不保证所有接收者都执行业务成功;跳过、失败或超时等状态也可能进入收尾流程。2
4 goAsync() 能否改变上述结论?
不能把 goAsync() 当作超时豁免。 对需要完成确认的投递,未调用它时,通常由framework在 onReceive() 返回后收尾;调用后,业务代码负责通过 PendingResult.finish() 完成这次处理。6 8
已有的超时预算不会因此取消或重新开始,onReceive() 返回也不再等于该次处理已完成。广播耗时的计算亦不是简单给方法体单独计时,计时起点与系统投递有关。7 8
另一方面,goAsync() 只是交出当前 PendingResult,没有把 mAssumeDeliveredHint 改成 false。对于原本“假定已投递”的场景,它不会反向要求系统重新建立完成等待或启动计时器。使用后仍应按 API 约定调用 finish()。6
不受本次广播计时器限制,也不是任务可靠完成的保证。 系统是否保留进程、任务中断后如何恢复,是独立问题。持续较久、需要重试或可靠完成的工作,应通过 JobScheduler 等合适的后台任务机制安排,而不是把 onReceive() 当作长期任务容器。9
结论
“
BroadcastReceiver#onReceive()执行耗时操作一定会导致 ANR”不是准确的机制描述。 在 AOSPandroid-15.0.0_r1的现代广播队列中,需要先判断这一次投递是否要求完成确认,以及是否实际启动了超时检查。1 2普通
sendBroadcast()投递给动态接收者,满足“动态注册+无序+无framework最终回调”,不会启动本次广播的处理超时检查。有序广播的业务接收者、无序广播的 Manifest 接收者,以及携带framework完成回调的无序投递,则需按条件接受超时检查。1 2 3 4“没有本次广播的超时检查”与“可以无限耗时且一定执行成功”是两回事。理解源码例外,是为了准确判断 ANR 成因,而不是取消对广播处理耗时和任务生命周期的管理。 2 8 9
补充前/后台广播判断逻辑
在判断前/后台广播时,调用的方法是BroadcastRecord.isForeground(),其具体实现如下:
// BroadcastRecord.java
boolean isForeground() {
return (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0;
}
所以,这里判断的是广播 Intent 是否携带 FLAG_RECEIVER_FOREGROUND,不是判断发送方或接收方应用是否处于前台,也不是判断广播是否有序。
另外针对具体的超时时间配置是 AMS 创建广播队列时初始化的,运行时还可以被 Settings.Global 中的配置覆盖。
ActivityManagerService.Injector#getBroadcastQueue()
-> `BroadcastQueueModernImpl`构造方法中
mFgConstants = Objects.requireNonNull(fgConstants);
mBgConstants = Objects.requireNonNull(bgConstants);
==> BroadcastConstants会监听对应的Settings.Global配置,读取其中的 bcast_timeout
前台对应:Settings.Global.BROADCAST_FG_CONSTANTS
后台对应:Settings.Global.BROADCAST_BG_CONSTANTS
配置的值的单位是ms,并且会直接覆盖使用,不会再乘硬件倍率
还是同一个源码版本上,默认值是:前台广播 10 秒,后台广播 60 秒,再乘以硬件超时倍率。定义在:
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
static final int BROADCAST_FG_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
static final int BROADCAST_BG_TIMEOUT = 60 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
单位是毫秒,所以倍率为 1 时,分别是 10 秒和 60 秒。
Build.HW_TIMEOUT_MULTIPLIER 定义在:
// frameworks/base/core/java/android/os/Build.java
public static final int HW_TIMEOUT_MULTIPLIER =
SystemProperties.getInt("ro.hw_timeout_multiplier", 1);
也就是说,它读取系统属性 ro.hw_timeout_multiplier,没有配置时使用 1。
因此,在倍率为 1、没有配置覆盖时,对应关系如下:
| 广播 Intent 的标志 | 选择的配置 | 基础默认值 |
|---|---|---|
带 FLAG_RECEIVER_FOREGROUND | mFgConstants.TIMEOUT | 10 秒 |
不带 FLAG_RECEIVER_FOREGROUND | mBgConstants.TIMEOUT | 60 秒 |
因此, “有序广播一定是 10 秒、无序广播一定是 60 秒”也是错误的理解;是否有序和选择哪一档超时时间,是不同的判断维度。
此外设置的超时时间,是基础超时,不一定是最终 ANR时间。Android 官方文档给出的 Android 14 及以上默认范围是:前台优先级广播 10~20 秒,后台优先级广播 60~120 秒,是否延长取决于进程是否缺乏 CPU 执行机会。
参考源码与文档
源码链接固定到 android-15.0.0_r1;Android Developers 文档用于公共 API、广播计时与生命周期说明。源码节选来自 AOSP,遵循原文件的 Apache License 2.0 许可。文中推导与说明不替代目标设备源码。1 2
| 编号 | 源文件或文档 | 核心定位 |
|---|---|---|
| [1] | BroadcastRecord.java | isAssumedDelivered();ordered = _serialized |
| [2] | BroadcastQueueModernImpl.java | dispatchReceivers();scheduleResultTo();DELIVERY_TIMEOUT 与 appNotResponding() |
| [3] | ContextImpl.java | sendBroadcast();sendOrderedBroadcast() |
| [4] | ActivityManagerService.java | LocalService.broadcastIntentWithCallback() |
| [4a] | ActivityManagerInternal.java | 无序投递与完成回调的接口约定 |
| [5] | Notifier.java | sendWakeUpBroadcast();sendGoToSleepBroadcast() |
| [6] | BroadcastReceiver.java | PendingResult.sendFinished();goAsync() |
| [7] | BroadcastReceiver API | setResult();goAsync() |
| [8] | Diagnose and fix ANRs | 广播超时与计时起止 |
| [9] | Broadcasts overview | 广播生命周期与后台任务 |