BroadcastReceiver#onReceive()执行耗时操作一定会导致ANR?

3 阅读13分钟

最近项目上不忙,用AI来夯实基础,其中就聊到广播的超时。看到的很多技术资料都说,前台广播10s超时,后台60s超时。具体的逻辑也没有去看,但是大家约定俗成的认为不能在onReceive()方法中执行耗时操作,所以一般也很少遇到广播导致的超时。今天跟AI对话了一下,才发现对于广播超时这一块有很大的误解。根据多轮聊下来的结果,然后整理记录了这篇文章。虽然感觉可能用不上,但是在AI发展的越来越快的当下,技术的深度和经验是保持不容易淘汰和替代的防火墙吧(个人感觉)。继续夯实基础吧~

回到文章主题。

BroadcastReceiveronReceive()中执行耗时操作一定会导致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

投递场景orderedresultToassumeDelivered本次广播处理超时检查
普通 sendBroadcast() → 动态注册的接收者falsenulltrue不启动
普通 sendBroadcast() → Manifest 注册的接收者falsenullfalse按条件启动
有序广播 → 链中的业务接收者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,因此后者不满足第一个条件1assumeDelivered 的含义是“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 接收者时,第一个条件不成立,得到 false1

所以,“普通 sendBroadcast()+动态接收者”才是完整结论,不能省略接收者类型。 即使是同一次发送,对不同接收者也可能有不同的超时处理方式。1 2

2.2 有序广播:不需要最终结果,也仍需等待链中接收者完成

例如:

context.sendOrderedBroadcast(intent, null)

这里的 nullreceiverPermission,这个重载没有提供最终结果回调,但仍设置了 ordered = true。因此,即使 resultTo == null!ordered 也已是 false,链中业务接收者不会进入“假定已投递”路径。1 3

有序广播需要根据当前接收者的完成状态推进后续投递。满足计时条件后,处理超过预算且未完成,可能触发广播超时 ANR。是否向发送方返回业务结果,不是唯一决定因素。 2

使用带 resultReceiver 参数的有序广播重载,可以接收通过 setResult() 等接口传递的最终业务结果;但不是传入这个参数之后,有序广播才开始需要完成确认。3 7

2.3 最终结果回调本身,是另一条投递路径

不要把广播链中处理业务的接收者发送时传入的最终 resultReceiver混为一谈BroadcastQueueModernImpl.scheduleResultTo() 向应用调度最终结果回调时,直接使用 assumeDelivered = true2

因此,有序广播链中的业务接收者按条件接受超时检查,不代表最终结果回调的 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 = false4 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 = false1 5

由此可见,单凭 isOrderedBroadcast() == false,不足以判断没有广播超时检查。1 2

3.3 省略第二个条件,会破坏什么?

假设将判断改成“动态接收者且无序”,带完成回调的无序投递也会被提前标记为完成。结合 dispatchReceivers() 的处理可以推导:framework可能在接收者实际尚未完成时,就推进最终回调,破坏原本需要的完成等待关系。 这就是 resultTo == null 不能省略的原因。1 2 4

这里的“完成通知”指framework按规则收尾,不保证所有接收者都执行业务成功;跳过、失败或超时等状态也可能进入收尾流程。2

4 goAsync() 能否改变上述结论?

不能把 goAsync() 当作超时豁免。 对需要完成确认的投递,未调用它时,通常由frameworkonReceive() 返回后收尾;调用后,业务代码负责通过 PendingResult.finish() 完成这次处理。6 8

已有的超时预算不会因此取消或重新开始,onReceive() 返回也不再等于该次处理已完成。广播耗时的计算亦不是简单给方法体单独计时,计时起点与系统投递有关。7 8

另一方面,goAsync() 只是交出当前 PendingResult,没有把 mAssumeDeliveredHint 改成 false。对于原本“假定已投递”的场景,它不会反向要求系统重新建立完成等待或启动计时器。使用后仍应按 API 约定调用 finish()6

不受本次广播计时器限制,也不是任务可靠完成的保证。 系统是否保留进程、任务中断后如何恢复,是独立问题。持续较久、需要重试或可靠完成的工作,应通过 JobScheduler 等合适的后台任务机制安排,而不是把 onReceive() 当作长期任务容器。9

结论

  1. BroadcastReceiver#onReceive() 执行耗时操作一定会导致 ANR”不是准确的机制描述。 在 AOSP android-15.0.0_r1 的现代广播队列中,需要先判断这一次投递是否要求完成确认,以及是否实际启动了超时检查。1 2

  2. 普通 sendBroadcast() 投递给动态接收者,满足“动态注册+无序+无framework最终回调”,不会启动本次广播的处理超时检查。有序广播的业务接收者、无序广播的 Manifest 接收者,以及携带framework完成回调的无序投递,则需按条件接受超时检查。1 2 3 4

  3. “没有本次广播的超时检查”与“可以无限耗时且一定执行成功”是两回事。理解源码例外,是为了准确判断 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_FOREGROUNDmFgConstants.TIMEOUT10 秒
不带 FLAG_RECEIVER_FOREGROUNDmBgConstants.TIMEOUT60 秒

因此, “有序广播一定是 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.javaisAssumedDelivered()ordered = _serialized
[2]BroadcastQueueModernImpl.javadispatchReceivers()scheduleResultTo()DELIVERY_TIMEOUTappNotResponding()
[3]ContextImpl.javasendBroadcast()sendOrderedBroadcast()
[4]ActivityManagerService.javaLocalService.broadcastIntentWithCallback()
[4a]ActivityManagerInternal.java无序投递与完成回调的接口约定
[5]Notifier.javasendWakeUpBroadcast()sendGoToSleepBroadcast()
[6]BroadcastReceiver.javaPendingResult.sendFinished()goAsync()
[7]BroadcastReceiver APIsetResult()goAsync()
[8]Diagnose and fix ANRs广播超时与计时起止
[9]Broadcasts overview广播生命周期与后台任务