Appearance
广播:队列、注册时机与 Android 8 之后的限流世界
本页导读:广播是 Android 最古老也最被滥用的 IPC。本页讲广播在系统侧的真实投递模型(有序/无序、现代广播队列)、注册时机的合法窗口、Android 8 隐式广播禁令的影响面,以及 App Standby Bucket 与 Doze 如何把「秒达」变成「择机送达」。前置:进程模型、Service。
一、投递模型:一对多的 Binder 扇出
广播的发送端只有一个入口(sendBroadcast/sendOrderedBroadcast),但系统侧把它拆成一场精细的投递工程:
- 入队:
ActiveServices校验(发送者权限、接收者android:exported/权限要求)后生成BroadcastRecord,进BroadcastQueue;Android 10+ 默认走现代广播队列(BroadcastQueueModern+AnrHelper),前台/后台分队列、按进程 adj 与优先级排序。 - 提权投递:广播正在发给某进程时,该进程 adj 被临时提升(量级为 perceptible 档);投递给 receiver 时给一个短暂执行窗口用于起服务/FGS(约 10s 量级,
BroadcastQueue的 FGS 提权逻辑,以 AOSP 为准)。 - 计时:每个 receiver 挂独立超时(历史口径前台 10s、后台 60s;现代队列为动态
anrTimeout,随进程状态调整,以 AOSP 为准);超时即 ANR(见 ANR 页)。 - 合并与去重:对「同 action 同数据」的连发(历史上有 throttle 白名单机制),以及对 cached 接收者的攒批,都是为了对抗当年闹钟类 App 的广播风暴。
java
// frameworks/base/core/java/android/content/BroadcastReceiver.java(机制注释大意)
// onReceive() 运行于主线程,且被系统视为「前台承诺」:
// 超过超时未返回 → 广播 ANR。
public final PendingResult goAsync() { ... }
// goAsync() 把回执权交给你:返回的 PendingResult 必须最终 finish(),
// 期间广播队列继续投递后续 receiver —— 这是唯一合法的「onReceive 里跑异步」姿势。onReceive 里没有「稍后」的概念:主线程执行、不能保证进程活着(后台 receiver 随起随杀)、禁止起子线程后撒手——要干重活就交给 goAsync 里完成回执、或立刻转投 WorkManager/JobScheduler。
队列内部状态可观测(现代队列把「排队-派发-回执」全暴露了出来):
bash
adb shell dumpsys activity broadcasts | head -40
# 典型关注点(字段名以版本为准):
# Modern Queue / Historical Broadcasts 分段
# BroadcastRecord{... act=... dispatchTime=... anrTimeout=...}
# receiver 进程不在时是否被标记为「延迟派发」二、有序 vs 无序:两条投递语义
| 维度 | 普通广播(无序) | 有序广播 |
|---|---|---|
| API | sendBroadcast | sendOrderedBroadcast |
| 到达顺序 | 系统按队列/adj 批量投,无保证 | 严格按 receiver priority(高→低)逐个投 |
| 可修改/可截断 | 否 | 是:resultData/resultExtras 传递,abortBroadcast() 截断 |
| 回执链 | 无 | resultTo 指定「链尾」接收方 |
| 典型场景 | 刷新缓存、进程内解耦 | 系统广播的优先级让渡(如 NEW_PICTURE、短信类) |
priority范围 -1000~1000,默认 0;只对有序广播有意义——把它用在普通广播上指望抢顺序,是面试与线上双重翻车点。- 有序广播链上每个 receiver 的超时不累计,但链整体拉长会放大首末端的感知延迟;系统自身很少再用有序广播做新功能。
- 应用间自造的有序广播请立刻换掉:优先级可以被别人抢,链可能被中途 abort,2026 年它是遗产而非方案。
有序链的最小示例(含链上信箱的正确用法):
kotlin
// 注册端:priority 只在有序广播里起作用
val filter = IntentFilter("com.example.ACTION_SCORE").apply { priority = 100 }
registerReceiver(scoreReceiver, filter, Context.RECEIVER_EXPORTED)
class ScoreReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val score = intent.getIntExtra("score", 0)
resultData = "score=$score" // 写回给链上下游
if (intent.hasExtra("terminate")) abortBroadcast() // 截断后续 receiver
}
}链上语义提醒:resultData/resultExtras 是链上共享的可变信箱;任何一个 receiver 抛异常都会让后续 dispatch 变得不可预期,链式设计的脆弱性可见一斑。
三、注册时机与 exported:三档位、一条红线
| 注册方式 | 存活期 | Android 8 后能收的隐式广播 | 注意 |
|---|---|---|---|
| manifest 静态 receiver | 随安装 | 几乎全部被禁(豁免清单除外) | android:exported 必须显式(有 filter 时,target 31+) |
registerReceiver 动态 | 进程存活期 | 可以 | target 34+ 非系统广播必须声明 RECEIVER_EXPORTED/NOT_EXPORTED |
RECEIVER_VISIBLE / 系统特权类 | 特殊 | 特殊 | device owner、系统应用专属,三方勿念 |
- 红线一:manifest receiver 的
onReceive只有约 10s 生命周期窗口,进程可能随广播结束立刻回收;在里面bindService、开线程池、读大文件都是事故源。 - 红线二:动态 receiver 泄漏是内存泄漏重灾区——注册在 onCreate 就必须在 onDestroy 解绑,注册在 Fragment 生命周期里最容易漏。
- 系统广播 vs 应用自定义广播:前者由 AOSP
AndroidManifest声明protected-broadcast,别人伪造不进来;你 App 的 action 谁都能sendBroadcast,默认不可信(第四节细说)。
动态注册的标准姿势(target 34+ 少了 flags 直接抛异常):
kotlin
class MainActivity : AppCompatActivity() {
private val screenOff = object : BroadcastReceiver() {
override fun onReceive(c: Context, i: Intent) { pausePlayback() }
}
override fun onStart() {
super.onStart()
registerReceiver(screenOff, IntentFilter(Intent.ACTION_SCREEN_OFF),
Context.RECEIVER_NOT_EXPORTED) // 只收系统广播也要显式标记
}
override fun onStop() {
unregisterReceiver(screenOff) // 漏解注册 = Activity 泄漏经典款
super.onStop()
}
}四、Android 8:隐式广播的「断头铡」
Android 8.0(API 26)起,manifest 注册的 receiver 收不到绝大多数隐式广播——这是当年最大的行为变更,动机是整机广播风暴(几十个 App 同时为每一条网络变化开机)。
- 仍然可收:官方豁免清单内的系统广播(开机、时区、USB 附件、锁屏相关等,完整清单见官方「广播限制」页);显式广播(
setPackage/setComponent指定目标)不受限。 - 替代路径按需求选:动态注册(进程活着)、WorkManager(数据类)、JobScheduler 网络约束任务(网络变化早已不靠广播)、或让触发方改为显式投递。
ACTION_TIME_TICK:从来不在豁免清单神话里,但 Android 8+ 它变成仅系统可发的显式定向投递(给注册了 filter 的活跃进程),别把它当闹钟。- 验证手段:
adb shell cmd deviceidle推进状态 +cmd appops set ...之后观察投递;静态 receiver 收不到时先 dumpBroadcastQueueModern的队列(dumpsys activity broadcasts,输出格式以版本为准)。
bash
# 对照实验:显式定向必达,隐式广播看桶脸色
adb shell am broadcast -a com.example.ACTION_PING -p com.example
adb shell am broadcast -a com.example.ACTION_PING # 只有动态 receiver 能收到
# 设备空闲态对照:
adb shell cmd deviceidle whitelist -f com.example # 解除豁免,恢复限流五、App Standby Bucket:投递被「看人下菜」
Bucket(Android 9 引入模型,API 30 提供只读公开常量)是系统对每个 App 的 usage-stats 画像:
| Bucket | 判定画像 | 对广播/后台工作的影响 |
|---|---|---|
| ACTIVE | 正在用 | 基本无限流 |
| WORKING_SET | 常用但非当下 | 轻限频 |
| FREQUENT | 每天一两次 | 中限频 |
| RARE | 很少用 | 隐式广播延迟投递、Job 配额收紧 |
| RESTRICTED | 异常行为/用户设定 | 广播/任务显著延迟,接近「拒付」 |
关键认知:
Bucket 限制的不只是广播——JobScheduler 配额、Alarm 精度、FGS 豁免窗口一起收紧(具体阈值以 AOSP 与 OEM 为准);
对显式定向广播与自身 App 内广播,系统仍会投,但 cached 接收方可能被攒批;「延迟投递」的真实含义是:你的 receiver 会在系统认为划算的时刻醒来,而不是被丢弃;
用户可在 设置→电池→详细用量 里把 App 设为 RESTRICTED(或在开发者选项看 bucket)——测试限流行为的最快方法是
adb shell am set-standby-bucket(需特权);Doze 是另一维:深度 Doze 下标准广播批量积压到维护窗口释放(网络类、闹钟类系统广播有豁免),Bucket 与 Doze 叠乘,这就是「后台广播看起来玄学」的根源。
Bucket 的「延迟」在 ANR 视角有副作用:被攒批的广播集中释放时,多个 receiver 同刻苏醒,低端机瞬间卡顿甚至广播超时——「广播风暴复发」的现代版本。
text
示例广播 ANR(logcat 文案随版本变化):
ANR in com.example
Reason: Broadcast of Intent { act=com.example.ACTION_UPLOAD flg=... }
Error: android.app.RemoteServiceException: Context.startForegroundService() ...
排查顺序:dumpsys activity broadcasts 找该 BroadcastRecord
→ 比对 dispatch 与 finish 时间差 → 定位卡在哪个 receiver 的主线程六、应用侧的正确用法
跨进程通知:优先显式 Intent(
setPackage(自己)或setComponent),把 action 当字符串协议而不是广播——事实上LocalBroadcastManager早在 API 29 就被弃用,官方推荐 Flow/LiveData 做进程内。高频事件(进度、传感器):广播扇出是 O(接收者数) 的 Binder 轰炸,改成共享 ViewModel/Flow 或回调注册。
安全三件套:接收方校验来源(
isOrderedBroadcast/uid 校验)、发送方带权限(sendBroadcast(intent, permission)要求接收方持有)、自定义广播绝不回传敏感数据。降级路径:静态入口的 App(如扫码唤醒)要考虑「隐式收不到」的机型/版本差异,留显式深链兜底。
发送方也要带权限:
sendBroadcast(intent, RECEIVER_PERMISSION)让没该权限的 receiver 收不到,自定义 action 的安全边界要两头都设。
kotlin
// 自定义广播的双保险
context.sendBroadcast(
Intent("com.example.ACTION_DATA_READY").setPackage("com.example"),
"com.example.permission.RECEIVE_DATA") // 接收方必须在 manifest 声明该 permission七、常见误区问答
Q1:registerReceiver 之后进程退了,广播会丢吗? 会。动态 receiver 的存活期就是进程存活期;cached 进程被冻结时还要等解冻。要「必达」就走系统级通道(WorkManager/JobScheduler)或让对方以 bound service 长期在场。
Q2:ACTION_SCREEN_OFF/USER_PRESENT 还有必要 manifest 注册吗? 它们属于少数仍在豁免清单常客名单上的广播(以官方清单为准);但新代码更推荐 Activity 生命周期 + ProcessLifecycleOwner,别再为亮灭屏养常驻 receiver。
Q3:广播传大数据有什么问题? Binder 缓冲区 1MB-8KB 是硬墙,extras 超阈值直接 TransactionTooLargeException。传引用(content URI、文件路径),不传载荷。
Q4:能给别的 App 发广播并确认它收到吗? 确认不了——显式定向只保证「投递」,不保证对方被拉活、receiver 没崩。要闭环改用 bound service/AIDL 同步调用。
版本注记:基于 Android 16 / API 36;Android 8 差异:隐式广播 manifest 禁令生效;Android 10 差异:现代广播队列灰度;Android 13 差异:
RECEIVER_EXPORTED/NOT_EXPORTED对 target 33+ 强制;Android 14~16 差异:广播触发的 FGS 提权窗口持续收紧(以行为变更文档为准)。
本页小结:广播的现代画像——投递是带优先级的系统级调度,receiver 是 10s 的临时工;manifest 静态收隐式广播的路线在 Android 8 就基本封死,Bucket/Doze 又把「何时到」变成系统裁量;剩下的正当用法是低频、显式、带权限校验的定向通知,进程内解耦请回到 Flow。
参考链接
- https://developer.android.com/develop/background/broadcast-receivers
- https://developer.android.com/guide/topics/performance/app-standby-bucket(App Standby Bucket)
- https://developer.android.com/guide/components/broadcast-exceptions(隐式广播豁免清单)
- https://cs.android.com/android/platform/frameworks/base/+/main:services/core/java/com/android/server/am/BroadcastQueueModern.java
- 站内:进程模型、Service、ANR 页