如果你打算把 Android 广播这套机制彻底吃透,源码层面有两个最合适切入的点:一个是动态注册的调用链,一个是发送方的调用链。只要把这两条链走通,剩下的静态注册、有序广播、粘性广播、并发派发全是同一套逻辑上的变体,看代码会轻松很多。
这篇文章以 API 29(也就是 Android 10)的实现为基准,逐层拆开 Broadcast 注册和发送的源码过程。不管你是刚开始啃源码,还是已经写过不少广播相关的业务代码,都可以照着这个思路把核心调用链画出来,以后再遇到收不到广播、重复收到、ANR 这类问题,就不再是靠猜,而是能顺着调用栈定位到具体哪一步出了问题。
1. 源码阅读前的准备工作
1.1 动态注册和静态注册先分清
广播接收者有两种存在形态,一种是写在 Manifest 里的<receiver>标签,由系统在应用安装和启动阶段解析;另一种是在代码里调用registerReceiver注册的,由当前进程运行时管理。二者虽然最终都会变成系统侧的一个“接收记录”,但注册入口完全不同,在源码里对应的类也不一样。
- 静态注册:解析工作集中在 PackageManagerService 侧,最终由 ActivityManagerService 生成管理条目。
- 动态注册:直接走 ContextImpl 到 ActivityManagerService 的 binder 调用链。
理解这一点后再看源码,思路会清晰很多。很多人被源码劝退的原因,就是把两类注册混在一起看,结果到处是分支,不知道哪条路才是当前的流程。
1.2 需要提前掌握的几个关键类
看这套源码之前,最好先把下面这些类的关系记在脑子里。它们是整个广播链路里的核心零件。
| 类名 | 角色说明 |
|---|---|
| ContextImpl | 应用侧真正的上下文实现,注册、发送的入口在这里 |
| ActivityManagerService | 系统侧的大管家,所有广播的注册和分发都要经过它 |
| RegisteredReceiver | AMS 里表示一个动态注册接收者的内部类 |
| ReceiverList | 一个进程里所有动态注册接收者的列表 |
| BroadcastFilter | 动态注册接收者对应的 IntentFilter 封装 |
| BroadcastQueue | 系统侧负责排队和派发广播的队列 |
| BroadcastRecord | 一条广播记录的封装,携带接收者列表 |
| IIntentReceiver | 系统侧回调应用侧接收者的 binder 接口 |
API 29 里广播的主要逻辑还是集中在 ActivityManagerService 和 BroadcastQueue 这两个类。前者负责登记和匹配,后者负责派发。分清这个界限之后,你会发现后面读代码基本就是两个循环:怎么登记、怎么广播。
1.3 阅读环境和版本选择建议
我建议直接拉取 android-10.0.0_r1 对应的源码,配合 Android Studio 创建一份 AOSP 工程来跳转阅读,或者直接用线上源码阅读站点。为什么不建议读最新的?因为 Android 10 到 Android 14 之间广播框架经历过多次调整,比如 BG 广播限制、自定义权限校验、派发队列的优化,这些改动常把核心路径夹杂很多安全校验,读起来效率低。API 29 的代码结构相对规整,适合建立整体认知。
读的时候思路要跟着 Binder 调用走。应用进程到系统进程的边界是 ActivityManager 这个接口,只要看到跨进程调用,就要意识到接下来已经切到了系统服务侧。
2. 动态注册:从 registerReceiver 到 AMS 落库
2.1 应用侧入口的调用链
应用侧调用registerReceiver,走的是 ContextWrapper 的代理方法,最终落到 ContextImpl。以 API 29 为例,ContextImpl.registerReceiver方法最终会调用一个核心的内部方法registerReceiverInternal,它做了三件关键的事:
- 根据传入的 BroadcastReceiver 和 IntentFilter 创建一个 ReceiverDispatcher(内部包含 IIntentReceiver 的 binder 实体);
- 通过 ActivityManager 的 binder 代理调用系统侧
registerReceiver; - 如果注册时有
BR_OPTIONS这类 flag,会在系统侧返回时触发一次本地的 sticky 检查。
关键这里 binder 的封装类叫IIntentReceiver,它真正的实体是ReceiverDispatcher.InnerReceiver,这是系统进程反过来通知应用进程的通道。代码的大致形态是:
// ContextImpl.java (API 29) private Intent registerReceiverInternal(BroadcastReceiver receiver, ...) { IIntentReceiver rd = null; if (receiver != null) { rd = new ReceiverDispatcher(receiver, context, handler, ...).getIIntentReceiver(); } // 调用系统侧注册 return ActivityManager.getService().registerReceiver(...); }这里有一个很实用的观察点:ReceiverDispatcher一旦创建,就和当前上下文的 Binder 通道绑定。如果你在 Activity 里注册了同一个 receiver 对象多次,系统侧会创建或复用对应的 ReceiverList,但 dispatcher 可能不同。在源码里看,多次调用同样参数的注册,最终 AMS 会判断同一个进程内是否已经有相同的接收者记录,进而决定是新增还是更新。
2.2 AMS 侧 registerReceiver 做了什么
进入 ActivityManagerService 之后,流程就变成了系统侧的登记逻辑。registerReceiver方法非常长,但主线其实只有几条:
第一,确认调用者的身份。AMS 会通过 Binder.getCallingUid 和 getCallingPid 拿到调用方的 uid、pid,这个信息后面所有的权限判断和进程锁定兜底都要用。
第二,如果不是系统进程,AMS 会把调用方的进程信息拿到,并插入mProcessNames对应的进程记录。这里也包括了进程死亡后自动移除接收者的逻辑。
第三,处理 sticky(粘性)广播。这里要做一次 IntentFilter 匹配,如果当前系统里已经存在和这个 filter 匹配的粘性广播,就会把这个 sticky Intent 立刻返回给调用方,作为注册的结果。这就是为什么动态注册粘性广播时,注册方法会直接返回一个 Intent 的原因。
第四,把接收者真正登记到两个结构中:
mRegisteredReceivers:以IBinder为 key 的 map,记录每个 binder 对应的ReceiverList;mReceiverResolver:一个 IntentResolver,用于后续发送广播时的动态接收者匹配。
真正的核心代码是构建ReceiverList和BroadcastFilter。前者代表这个进程的一批接收者集合,后者代表当前这次注册的意图过滤条件,并且记录注册时的权限要求。
// ActivityManagerService.java (API 29) ReceiverList rl = mRegisteredReceivers.get(receiver); if (rl == null) { rl = new ReceiverList(this, callerApp, ...); mRegisteredReceivers.put(receiver, rl); } BroadcastFilter bf = new BroadcastFilter(filter, rl, ...); rl.add(bf); mReceiverResolver.addFilter(bf);到了这里,动态注册就完成了。以后任何符合条件的广播发送过来,mReceiverResolver都能匹配到这条记录。
2.3 动态注册的几个隐藏细节
初次读这部分源码很容易忽略两个点:一是注册时传入的handler参数,它决定了 onReceive 执行在哪个线程;二是注册时带权限和不带权限的区别。
如果你传入了 Handler,那么后续系统侧回调到应用进程时,ReceiverDispatcher会把这个回调 post 到对应 Handler 所在的 Looper,否则默认执行在主线程。源码里ReceiverDispatcher持有一个 handler,内部类InnerReceiver收到调用后,如果不是同步派发模式,会通过 handler 发出一条消息。
权限这块,注册时如果传了broadcastPermission,那么发送方必须持有该权限才能把广播给你。源码里这个权限会被存在BroadcastFilter.requiredPermission字段上,后续分拣匹配时做权限校验。
另一个细节是进程被杀之后的自动清理。AMS 在appDiedLocked等进程清理流程里,会找到这个进程对应的所有ReceiverList,从mRegisteredReceivers和mReceiverResolver中移除。你可以理解为:只要进程还活着,动态注册一直有效;进程死掉,系统帮你就地清理。
我在源码里第一次读到这里时,唯一的感受是:动态注册的“生命周期绑定进程/组件”不仅仅是设计理念,而是真实地反映在数据结构的增删上。
3. 静态注册:谁在系统启动初期就把你登记好了
3.1 PackageManagerService 解析 Manifest 的阶段
静态注册之所以叫静态,是因为 Manifest 中<receiver>标签会被系统在安装或者开机扫描时解析成一个个ActivityInfo,更准确地说,是 receiver 会存放在 ApplicationInfo 的 receivers 数组中。
这个阶段的工作主要发生在 PackageManagerService 里。它会解析每个应用的AndroidManifest.xml,把<receiver>对应的ActivityInfo和其中的<intent-filter>提取出来,放到PackageParser的解析结果里。这里要注意,静态 receiver 本身没有代码,它只是一个入口配置,完整的 receiver 组件信息需要系统后续组合。
3.2 从 PMS 到 AMS 的落位过程
静态 receiver 并不像动态注册那样在运行时单独登记,而是在 AMS 构造和 PMS 建立连接时,从 PMS 加载所有已安装应用信息,然后逐个处理 receiver。API 29 的ActivityManagerService里有一个mReceiverResolver,专门用来匹配静态 receiver。这部分数据的源头是 PMS 的setPackageInstalledInfo和系统启动时的scanDirLI。
实际操作中,静态 receiver 在 AMS 侧的形态是一张“意图匹配表”,每个条目对应一个 receiver 组件和它的 intent-filter。由于没有动态注册的 binder 通道,这些静态 receiver 在广播到来时,如果需要拉起一个还没启动的应用进程,就会触发进程创建流程,也就是 cold start 启动。
这也是静态广播最大的特殊性:广播可能触发一个未启动应用的进程创建。而动态注册的接收者只可能属于当前已经运行的进程,不存在冷启动这个分支。
3.3 隐式广播限制:Android 8.0 之后的暗坑
如果你在 API 29 上开发,需要特别重视:从 Android 8.0 开始,绝大多数隐式广播(即没有明确指定包名/组件名的广播)不允许在 Manifest 里静态注册。系统只放行白名单内的隐式广播,比如BOOT_COMPLETED、LOCALE_CHANGED、USB_STATE_CHANGED等。
这个限制在源码层面不是直接禁止解析,而是在广播发送阶段做了一次“按需过滤”。可以理解为:即使你的 Manifest 写了一个隐式广播行为,系统侧也会根据 package visibility(包可见性)规则或者 receiver 组件解析的规则,在匹配时直接把它过滤掉。很多人遇到“静态广播收不到”的问题,绝大多数情况都和这个限制有关。
要避免这个坑也很简单:能动态注册就动态注册,或者把广播改为显式发送(指定 packageName 或 component)。在源码层面,broadcastIntentLocked对显式广播的匹配逻辑较少限制,行为更可控。
4. 广播发送:核心派发链路全解析
4.1 sendBroadcast 的起点与参数包装
发送广播的入口是ContextImpl.sendBroadcast,它最终也会走 ActivityManager 的 binder 代理,调用AMS.broadcastIntent。与注册流程不同的是,发送方法会把权限、userId、flag 等参数全部打包。
sendBroadcast(intent)没传权限,系统侧会当成无权限要求的广播处理。而sendBroadcast(intent, permission)会把权限字符串传到broadcastIntentLocked,在匹配阶段做权限检查。这两个方法在源码里的整条链路是完全一样的,区别只在最后派发时带不带权限约束。
4.2 AMS 怎么处理一次广播请求
AMS.broadcastIntent这个方法做几件事:
- 解析 Intent 的 component 和 action,确定是显式广播还是隐式广播;
- 做基本的 caller 身份校验和权限检查,比如有的广播只允许系统发;
- 进入
broadcastIntentLocked方法,这里才是真正决策一切的地方。
broadcastIntentLocked里首先会把 Intent 做一次“规范化”:如果原 Intent 为 null,会构建一个新的 Intent;同时会把调用者的包名、uid、userId 等信息挂到 Intent 上。接下来就要决定接收者集合。
在 API 29 中,接收者的匹配分成两条线:动态注册的接收者从mReceiverResolver匹配,静态注册的接收者从mReceiverResolver里也能匹配到,但来源标识不同。AMS 内部会把两类匹配结果合并成一个接收者列表,作为这次广播的投递目标。
需要注意的是,如果之前设置过 sticky 广播,AMS 还会在新的发送过程中更新mStickyBroadcasts,并可能立即向当前正在注册的接收者补发这个粘性广播。这一块逻辑很长,但是理解时只需要一个概念:sticky 广播是“当时不留名,注册时补发”。
4.3 广播如何进入 BroadcastQueue
匹配完成之后,AMS 并不是立即开始挨个通知接收者,而是先把广播记录放进BroadcastQueue。API 29 的 AMS 持有两个 BroadcastQueue:
mFgBroadcastQueue:前台广播队列,超时短,优先级高;mBgBroadcastQueue:后台广播队列,超时长,逻辑和派发策略更“节省资源”。
进入哪个队列由broadcastIntentLocked的顺序标志决定,系统根据 broadcast 的 flag、接收者进程是否前台,以及其他历史状态来选。
队列里的单元叫BroadcastRecord。它记录了这次广播的所有接收者列表、发送者 uid、权限、Intent、ordered 标志等信息。
如果是普通无序广播,AMS 会把它加到该队列的mParallelBroadcasts列表里,表示“这条广播可以平行分发”,系统会尽快把目标接收者都调一遍。如果是有序广播,AMS 会调用:
// BroadcastQueue.java enqueueOrderedBroadcastLocked(broadcast);然后队列进入processNextBroadcastLocked的循环逻辑,一个一个按顺序派发,上一个没执行完,下一个不会发出去。
4.4 普通广播和有序广播的派发差异
普通广播(sendBroadcast)虽然叫无序,但源码里并不是异步地发一百个线程,它也要经过processNextBroadcastLocked,只不过由于BroadcastRecord.ordered = false,整个队列会用“一次性分发给所有动态接收者”的方式处理,然后再逐个拉起静态接收者。
有序广播(sendOrderedBroadcast)则完全不同,queue 里必须串行处理。每次只找一个 target receiver,调用完还要等它返回,再进入下一轮循环。这意味着有序广播如果有个接收者迟迟没响应,后续接收者都会阻塞。源码里有一个mTimeout机制,超时后会把它标记为跳过,继续走下一个接收者。
这套机制落到底层,就是BroadcastQueue的processNextBroadcastLocked在选择下一个 receiver 时的状态机控制。标志位mPendingBroadcast就是用来记录当前正在等待的有序广播的。
还有一点必须记住:前台队列和后台队列的超时阈值不同,但处理逻辑完全一样。如果你发的是后台广播,又在 onReceive 里做了耗时操作,容易触发 ANR,这类问题的源头就在这里。
4.5 粘性广播与显式广播的特殊处理
粘性广播发送时,broadcastIntentLocked会把 intent 存进mStickyBroadcasts(按 userId 存)。发送完后,广播本身会继续派发,同时系统保留了这份“广播内容”,让之后注册的接收者可以立即获知最近的粘性广播状态。
显式广播的匹配逻辑更有意思。由于它显式指定了组件或包名,系统在匹配静态接收者时不需要做复杂的 IntentFilter 匹配,直接按 component 找目标即可。这类广播不受隐式广播限制规则影响,所以如果你业务上非要让人随时都能收到广播,把广播改显式是绕过系统限制最稳的办法。
从成本角度说,显式广播在源码层面也更省,因为它跳过了大量 resolve 匹配过程,尤其在应用数量多的时候,功耗和时间都能明显降下来。
5. 回调链路:AMS 怎么把消息送回应用进程执行 onReceive
5.1 系统侧调用应用侧 binder 的过程
广播进入派发阶段后,AMS 通过 BroadcastQueue 找到目标接收者。对于动态注册的接收者,会直接拿到注册时留下的 IIntentReceiver binder 对象,通过它回调到应用进程。
对于静态注册的接收者,如果对应进程没起来,AMS 会先通过startProcessLocked启动它。启动完成后会等待该进程的 ApplicationThread 绑定好,再通过对应进程的 IApplicationThread 去调度接收器。
动态和静态两条路最终都会在应用进程侧汇合到一个入口:ActivityThread。区别只在于,动态接收者走的回调是IIntentReceiver,静态接收者走的是IApplicationThread.scheduleReceiver。前者会把消息派发给 ReceiverDispatcher,后者则直接交给 ActivityThread 的内部 ReceiverData。
5.2 ActivityThread 里如何调度 onReceive
如果你是跟着源码走,会发现所有 binder 回调到了 ActivityThread 之后,都会转换成 Handler 消息,发送到主线程的mH(ApplicationThread.H)对象。发送消息的类型是:
RECEIVER:静态接收器的调度消息;CALLBACK:动态接收器的调度消息,具体执行由 ReceiverDispatcher 完成。
最终都是主线程执行,所以 onReceive 里默认不能做网络请求等耗时操作。API 29 源码里,主线程的 Handler 在执行这段逻辑时,会先调用ActivityThread.handleReceiver,其中会加载 receiver 类、实例化对象,然后调用receiver.onReceive(context, intent)。
对于动态注册,ReceiverDispatcher 会先把 onReceive 的执行包装成一个 Runnable,通过 handler 投递到线程。如果你注册时传了handler,就用这个自定义 handler 执行;如果没传,由主线程 Handler 执行。
我在这个地方建议初学者重点看一下“binder 线程”和“主线程”的分界。你注册时传入的 ReceiverDispatcher,内部并不是直接执行 onReceive,而是通过 Handler 切换线程。这个切换过程就是很多“回调没执行”问题的关键排查点。
5.3 onReceive 执行完之后的反馈
onReceive 执行完后,应用进程是否要回复系统进程?答案是必须回复。源码中,广播分发是有一个“完成信号”的机制,系统侧需要知道这次回调是否结束,才能决定是否继续派发下一个有序广播。
这个反馈路径就是IIActivityManager.finishReceiver,应用进程在执行完 onReceive 后,通过 ActivityManager 的代理通知系统侧“这次广播处理完了”。有序广播只有收到这个反馈,才会继续处理下一个接收者。
如果 onReceive 里调用goAsync(),那么应用进程会进入异步模式。ReceiverDispatcher 会在 onReceive 返回后不立刻发完成信号,而是等异步任务结束后调用finish再回系统。一个典型的 bug 是 goAsync 后忘记在异步任务里收尾,导致系统侧一直等不到完成信号,最终触发超时逻辑,表现为广播流程后续全部阻塞,甚至 ANR。
6. 从源码角度排查广播常见问题
6.1 收不到广播怎么定位
结合前面的调用链,收不到广播的可能性其实集中在这几个节点:
- 发送方和接收方的 IntentFilter 不匹配,也就是
BroadcastFilter的匹配失败; - 隐式广播受到静态注册限制,你没有在源代码层面绕过;
- 发送方权限不够,AMS 在权限检查阶段就把你筛掉;
- 接收进程没起来,静态接收者还没来得及注册到位;
- 有序广播前面的接收者卡住了,你的接收者在后面排队。
排查这一步,最好的办法是看系统侧日志。AMS 在广播分发时都会打相关日志,尤其有序广播会有BroadcastRecord的收发状态,能明确看到一个广播卡在哪个接收者上。这类日志普通项目可能没开,但开发阶段建议开到系统级别再看。
6.2 动态注册重复回调
动态注册重复回调,最常见的原因就是注册了多次但只注销了一次。从 AMS 侧的数据结构看,同一进程同一个 binder 对象只会对应一个 ReceiverList,但如果你每次注册都持有不同的 Receiver 实例,系统侧可能把 filter 加到同一个进程的列表里。除非你已经做了 STRICT 的接口判断,否则很容易出现同一个 Intent 命中多个BroadcastFilter,进而被回调多次。
从源码角度理解,mReceiverResolver的匹配本来就是找所有符合条件的 Filter。你在同一个 Context 上注册了两条 filter 相同但 receiver 对象不同的广播,系统认为这是两次注册,自然都会命中。
解决的唯一正道是注册和注销成对调用,在onDestroy/onStop里注销,并保证 receiver 对象是单例。
6.3 goAsync 的坑与正确写法
如果 onReceive 里需要做冷启动后的一些延迟操作,很多人会想到goAsync()。源码里这个做法会让 ReceiverDispatcher 进入 pending 状态,因此系统侧不会发送 finishReceiver,而是等异步任务结束后你手动调finish()。
goAsync 必须在 onReceive 返回前调用,否则没有意义。这个约束在源码里有严格判定,即调用 goAsync 后,OldReceiver 的 finish 会变成由你的异步结果触发。如果你在 onReceive 返回后才调用 goAsync,源码里已经走完了 finishReceiver 过程,后续再去 finish 会无效,也大概率造成管理混乱。
正确范式是:
@Override public void onReceive(Context context, Intent intent) { final PendingResult result = goAsync(); new Thread(() -> { // 处理耗时事务 result.finish(); }).start(); }6.4 静态 receiver 冷启动为什么慢
静态接收者广播触发冷启动,是广播机制里最重的开销路径。AMS 需要先创建进程,等待进程启动,再把广播通过 IApplicationThread 调度进去。这个过程比动态注册多一次进程创建的开销。
如果广播接收者频繁触发且进程反复被杀,就会导致多次冷启动或启动风暴。源码里 AMS 对进程有缓存和优先级管理,但开发者仍然应避免用静态接收者做高频的实时业务。对这种场景,最好换成长连接或前台服务承载。
7. 我在实际跟踪源码时的一些心得
读这套源码对我帮助最大的一个时刻,是彻底搞清楚“注册”和“回调”是对称的:注册的时候,应用进程交付了一个 binder 给系统进程;回调的时候,系统进程通过这个 binder 把广播送回来。理解了这一对关系,再看registerReceiver和broadcastIntent的漫长代码,就能自动按“登记”和“通知”两个阶段划分,重点只看数据结构怎么存取。
调试广播类问题,我也建议大家养成一个习惯:不要只看应用侧 Log,要同时开系统侧日志。一个广播如果在应用侧没有任何反应,多半是根本没有派发过来,而不是 onReceive 里的代码写错。只有在系统侧确认派发到你的进程后,再去看应用侧逻辑才有意义。
另外,版本差异真的别忽略。API 29 里mParallelBroadcasts的处理方式和后续版本有差异,如果直接拿最新的代码思路去套老版本,容易误判。你如果只是做业务开发,不需要每个版本都精读,只要抓住 API 29 这一条主线,再了解后续版本的增量变化,就足够应付绝大多数日常问题了。