☰
Android广播机制源码解析:从注册到接收的完整调用链
2026/10/15 17:17:04 网站建设 项目流程

开头部分得先拉个远景。很多人学Android广播,查了大量博客,看到一堆注册、发送、接收的代码,但始终没搞清楚这些代码在执行时到底经过了哪些系统进程、哪些Binder调用、哪些消息队列。光知道registerReceiver和sendBroadcast怎么用,遇到“收不到广播”“顺序错乱”“进程被杀”这种线上问题,照样抓瞎。这篇文章基于Android 10(API 29)的AOSP源码,把动态广播从注册、发送到接收的完整调用链逐层拆开,把关键的类、方法、参数都点到位,适合刚接触源码的初级开发,也适合需要排查广播疑难杂症的中级开发。看完你会明白,广播这条链路本质上就是一次跨进程调用加一次消息调度,把它当成一条流水线去理解,源码就没那么难啃了。

1. 广播机制全景:注册、发送、接收到底是怎么串起来的

1.1 系统进程与App进程的分工模型

Android的广播机制是个典型的多进程协作场景。发送方App调用sendBroadcast,接收方App通过registerReceiver或者Manifest静态注册表达“我想听什么广播”。这两个App进程本身不直接通信,所有广播都汇入系统进程(system_server)里的ActivityManagerService,由它做匹配、排队、分发。接收方的进程可能还活着,也可能没起来,AMS要根据情况决定是直接投递到已运行的进程,还是先把接收者拉起来再投。

理解广播的关键,是先理解这三方各自的职责:发送方只负责把Intent塞给AMS;AMS负责拿这个Intent去匹配所有已注册的接收者,再按规则投递;接收方进程通过ApplicationThread接收AMS的消息,最终把BroadcastReceiver.onReceive回调到主线程。整条链路里,Binder负责跨进程通信,Handler负责进程内的消息调度,两者配合才完成了广播的整个生命周期。

1.2 动态注册与静态注册的本质区别

动态注册registerReceiver是把BroadcastReceiver实例和IntentFilter注册到AMS,注册后只要进程活着就能收广播,进程死了注册也就没了。静态注册则是通过Manifest里的<receiver>声明,由PackageManager在安装时解析并记录,AMS发广播时会通过collectReceiverComponents去PackageManager查询符合条件的组件,即使接收进程没启动,AMS也可以先把进程拉起来再投递。

从API 29源码来看,动态注册的核心类是BroadcastFilter,静态注册核心类是ResolveInfo对应的Receiver组件。AMS在broadcastIntentLocked里会分别查询两类接收者:动态的通过mReceiverResolver.queryIntent查,静态的通过collectReceiverComponents查。所以动态接收者和静态接收者在AMS里是两条独立的匹配路径,这也是为什么同一个广播动态注册能收到、静态收不到时,往往要先排查Manifest的声明和版本限制。

1.3 为什么选API 29的源码来看

Android 10(API 29)的广播相关源码处在一个比较有代表性的节点:它已经继承了Android 8.0后台执行限制、Android 9.0隐式广播限制,又比Android 12之后动辄强制的exported声明宽松不少,代码层面BroadcastQueue、BroadcastDispatcher等核心类也相对稳定。拿这个版本啃源码,既能看清机制的全貌,又不至于被最新的各种兼容策略干扰理解。

2. 注册过程源码全链路:从Context到AMS

2.1 ContextWrapper到ContextImpl的入口

日常代码里写的context.registerReceiver(receiver, filter),实际调用的是ContextWrapper.registerReceiver,它只是个壳,真正的实现在ContextImpl里。

// ContextWrapper.java @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { return mBase.registerReceiver(receiver, filter); }

mBase就是ContextImpl。在ContextImpl里,registerReceiver最终会走到registerReceiverInternal,这里有个容易被忽略的参数broadcastPermission——发送方要广播到这个接收者,必须持有这个权限。很多系统广播的接收者会对发送方做权限校验,就是靠这个字段。

private Intent registerReceiverInternal(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler, Context context, int flags, String[] requiredPermissions) { IIntentReceiver rd = null; if (receiver != null) { if (mPackageInfo != null && context != null) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } rd = mPackageInfo.getReceiverDispatcher(receiver, context, scheduler, mMainThread.getInstrumentation(), true); } else { ... } } try { final Intent intent = ActivityManager.getService().registerReceiver( mMainThread.getApplicationThread(), mBasePackageName, rd, filter, broadcastPermission, userId, flags, requiredPermissions); ... } catch (RemoteException e) { ... } }

这里面的ActivityManager.getService()就是AMS的Binder代理,IActivityManager接口是跨进程的桥梁。注册动作最终发生在AMS进程里,App进程只是把“我要注册”这个请求通过Binder发过去。

2.2 LoadedApk.ReceiverDispatcher:为什么需要这一层包装

注意getReceiverDispatcher这步。AMS进程里持有的是IIntentReceiver类型的Binder对象,而App进程里的BroadcastReceiver只是个普通Java对象,两者无法直接关联。LoadedApk.ReceiverDispatcher就是中间的适配层,它把BroadcastReceiver包装成一个InnerReceiver,这个InnerReceiver继承IIntentReceiver.Stub,能够在AMS跨进程调用时被唤醒。

static final class ReceiverDispatcher { final BroadcastReceiver mReceiver; final InnerReceiver mIIntentReceiver; final Handler mActivityThread; ... static final class InnerReceiver extends IIntentReceiver.Stub { final WeakReference<ReceiverDispatcher> mDispatcher; ... public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { ... mDispatcher.get().performReceive(intent, resultCode, data, extras, ordered, sticky, sendingUser); } } }

这里有个设计细节值得琢磨:ReceiverDispatcher用的是LoadedApk持有,而不是Context直接持有。LoadedApk跟App的包信息、ClassLoader绑定,生命周期比单个Activity长。这样同一个BroadcastReceiver被多次注册时,能复用同一个Binder对象,避免重复创建。

另外,InnerReceiver对ReceiverDispatcher用的是WeakReference。为什么?因为ReceiverDispatcher里持有BroadcastReceiver,而BroadcastReceiver往往又持有Context,Context又可能持有Activity。如果InnerReceiver强引用ReceiverDispatcher,AMS的ReceiverList里那个IIntentReceiver对象会一直强引用到App进程的Activity,导致Activity无法释放,内存泄漏就是这么来的。但AMS那边也是用WeakReference吗?不是,AMS的ReceiverList强持有Binder,如果Binder不被解除,确实会拖住App进程的对象。

所以规范做法是:onDestroy里必须unregisterReceiver。unregisterReceiver会把AMS里的ReceiverList移除,Binder引用断开,整个链路的强引用才能释放。

2.3 AMS.registerReceiver里面到底做了什么

Binder调用到达AMS后,进入ActivityManagerService.registerReceiver。这个方法在API 29里逻辑很长,核心步骤我拆出来讲。

第一,合法性检查。AMS会校验调用者、权限,如果注册者没有权限或者进程状态异常,会直接拒绝。代码里有一个enforceNotIsolatedCaller之类的检查,隔离进程不能随便注册广播接收者。

第二,sticky广播处理。sticky广播是sendStickyBroadcast发送的广播,会保存在AMS的mStickyBroadcasts里。注册时,如果IntentFilter里包含的action恰好有对应的sticky广播,AMS会把最后一条sticky Intent拿出来,稍后当成普通广播立即投递给新注册的接收者。这就是为什么你注册一个接收者,如果之前发过sticky广播,注册后立刻能收到一次。

Iterator<String> actions = filter.actionsIterator(); if (actions != null) { while (actions.hasNext()) { String action = actions.next(); stickies = mStickyBroadcasts.get(userId); if (stickies != null) { ArrayList<Intent> list = stickies.get(action); if (list != null) { if (intent == null) { intent = list.get(0); } ... } } } }

第三,注册接收者。AMS把每个动态注册的接收者封装成ReceiverList,存入mRegisteredReceivers,key是receiver.asBinder()。然后创建BroadcastFilter,加入ReceiverList,同时把BroadcastFilter加入AMS的mReceiverResolver。mReceiverResolver是个IntentResolver,负责后面广播发送时的匹配查询。

ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder()); if (rl == null) { rl = new ReceiverList(this, callerApp, receiver.asBinder(), pid, userId); ... mRegisteredReceivers.put(receiver.asBinder(), rl); } ... BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, permission, callingUid, userId, instantApp, visibleToInstantApp, flags); rl.add(bf); ... mReceiverResolver.addFilter(bf);

需要注意,同一个IIntentReceiver对象重复注册多次,不会新建ReceiverList,而是往同一个ReceiverList里添加多个BroadcastFilter。这解释了为什么同一个receiver重复注册没有崩溃,但反过来如果只想注销一次,AMS会认为你只注销了一个filter,另一个还在。日常开发里这种场景少见,但源码原理清楚了就不会困惑。

第四,投递sticky广播。前面如果找到了匹配的sticky Intent,AMS会调用deliverToRegisteredReceiverLocked,把广播直接投递给刚注册的接收者。这个投递其实是走了与正常发广播相同的分发路径,只是接收者列表就是当前这个新注册的接收者。

2.4 sticky广播的注册逻辑:到底谁在“记住”广播

我单独把sticky广播拎出来讲,因为它很容易踩坑。sendStickyBroadcast在API 21以后已经不推荐使用,但源码层面它仍然存在。sticky广播发送时,AMS在broadcastIntentLocked里会把Intent保存到mStickyBroadcasts,以userId为key,action为内层key,存储的是一个ArrayList<Intent>。后续注册接收者时,如果filter匹配到sticky action,就会取出列表里的第一条Intent,当作立即投递的广播。

要注意它的两个坑。第一,sticky广播会长期驻留在AMS内存里,直到显式移除。如果你发送了一个带敏感数据的sticky广播,后续任何应用注册对应action的接收者都能立即拿到这条数据,比普通广播泄露面大得多。第二,sticky广播只能由系统应用发送,普通应用调用sendStickyBroadcast会抛SecurityException。源码里AMS会对发送者做权限检查,非系统应用直接拒绝。

3. 发送广播全过程:sendBroadcast之后的代码旅程

3.1 sendBroadcast方法族背后的统一入口

发送广播的入口同样是ContextImpl。sendBroadcast(Intent intent)最终调用ActivityManager.getService().broadcastIntentWithFeature,最终进入AMS的broadcastIntent方法。

@Override public void sendBroadcast(Intent intent) { ... try { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getOpPackageName(), null, intent, null, null, 0, null, null, null, OP_NONE, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }

参数从左到右分别是:调用线程(ApplicationThread)、包名、feature、Intent、resolvedType、resultTo(有序广播的结果接收者)、resultCode、resultData、resultExtras、requiredPermissions、appOp、options、serialized(是否有序)、sticky(是否粘性)、userId。

注意requiredPermissions这个参数。发送方在广播里指定权限后,AMS在匹配接收者时,会检查接收者是否持有该权限;如果接收者没有申请对应权限,广播不会投递给它。源码里是在deliverToRegisteredReceiverLocked里做的权限校验。

3.2 broadcastIntentLocked:匹配接收者的核心

AMS的broadcastIntent在持锁状态下调用broadcastIntentLocked,这是广播机制最核心的一段代码。我按执行顺序来拆。

首先,处理一些特殊广播。比如ACTION_SCREEN_ON、ACTION_SCREEN_OFF这类系统广播有特殊的投递逻辑,会直接投递给特定接收者。还有appOp检查,如果有options里的appOp不符合,会直接返回。这些属于系统内部的特殊分支,普通应用广播几乎不会走到。

接着是接收者的收集。代码里分两条线:动态注册的接收者通过mReceiverResolver.queryIntent查询,得到List<BroadcastFilter>;静态注册的接收者通过collectReceiverComponents查询,得到List<ResolveInfo>。

List<Object> receivers = null; List<BroadcastFilter> registeredReceivers = null; if ((intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) { receivers = collectReceiverComponents(intent, resolvedType, callingUid, userId); } if (intent.getComponent() == null) { registeredReceivers = mReceiverResolver.queryIntent(intent, resolvedType, flags, userId); }

这里有几个标志位值得说明。FLAG_RECEIVER_REGISTERED_ONLY表示只投递给动态注册的接收者,不启动静态接收者;FLAG_RECEIVER_REPLACE_PENDING表示如果有相同的Pending广播,用新广播替换旧的;FLAG_RECEIVER_FOREGROUND表示以前台优先级投递。这些flag在BroadcastRecord里都会记录,分发时影响调度策略。

然后是sticky的处理:如果sticky为true,AMS会把Intent存入mStickyBroadcasts。同时如果广播有resultTo,且是sticky广播、resultCode != START_TASK_TO_FRONT等条件下,还需要额外处理结果。

再往后,代码里会创建BroadcastRecord,并选择放进哪个BroadcastQueue。API 29里AMS默认有两个队列:前台队列mFgBroadcastQueue和后台队列mBgBroadcastQueue。决定队列的因素有两个:一个是intent是否有FLAG_RECEIVER_FOREGROUND标志,另一个是发送进程是否处于前台。前台广播有更短的处理超时(10秒),后台广播超时是60秒。

final BroadcastQueue queue = broadcastQueueForIntent(intent); BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage, callingPid, callingUid, callingInstantApp, resolvedType, requiredPermissions, appOp, options, registeredReceivers, receivers, resultTo, resultCode, resultData, resultExtras, ordered, sticky, false, userId); ... if (ordered) { queue.enqueueOrderedBroadcastLocked(r); } else { queue.enqueueParallelBroadcastLocked(r); } queue.scheduleBroadcastsLocked();

3.3 BroadcastQueue与BroadcastRecord:广播的排队与调度

平行广播和有序广播在队列里的处理方式完全不同。平行广播(普通sendBroadcast)会被放进mParallelBroadcasts列表,一次性批量取出,逐个投递给所有接收者。投递的时候不等待接收者执行完,每个接收者都是独立异步投递。所以普通广播的接收者执行顺序在多个进程之间是不确定的。

有序广播则放进mOrderedBroadcasts列表,每次只处理一条。当前广播执行完(接收者调用finish()或者超时),才会处理下一条。这就是有序广播“串行执行”语义的底层实现。

BroadcastRecord是整个调度过程的数据载体,它记录了发送方信息、目标接收者列表、当前执行到的接收者下标、超时时间等等。在processNextBroadcastLocked里,AMS会从队列中取出BroadcastRecord,根据当前下标依次投递。

BroadcastRecord r = mPendingBroadcast; if (r == null) { ... r = mOrderedBroadcasts.get(0); ... } ... if (r.receivers == null || r.nextReceiver >= r.receivers.size()) { // 当前广播已处理完所有接收者,处理结果并进入下一条 cancelBroadcastTimeoutLocked(); ... addBroadcastToHistoryLocked(r); ... processNextBroadcastLocked(true); return; }

API 29里还有BroadcastDispatcher这个类,它在BroadcastQueue和真正投递之间做了一层调度,负责处理延迟广播、后台限制等策略。简单理解就是:BroadcastQueue决定了广播的执行顺序,BroadcastDispatcher决定了广播什么时候能真正投递出去。

3.4 匹配算法:一个Intent怎么找到它的接收者

匹配这块值得单说,因为它直接跟你写的IntentFilter对应。AMS的mReceiverResolver继承IntentResolver,queryIntent会根据Intent的action、type、scheme、data等属性,去匹配所有BroadcastFilter。

匹配规则里最容易踩的坑是:如果IntentFilter只写了action,没写type,但发送的Intent显式设置了type(比如setData、setType),那么action匹配成功但data匹配失败,接收者不会收到广播。反过来,filter写了type,发送的Intent没设置type,也匹配不上。代码里IntentResolver的匹配流程是matchAction和matchData都要过,最后还要看matchCategories。

实际上日常广播大部分只传action,所以大多数开发很少遇到data匹配问题。但系统广播里有相当一部分带data URI,比如网络状态变化的广播,如果你的filter误加了data相关属性,就会莫名收不到。排查的时候建议把IntentFilter尽量写简单,等定位到问题再一步步往上加条件。

4. 接收端:一条广播如何变成onReceive回调

4.1 ApplicationThread消息转发

当BroadcastQueue决定投递给某个接收者时,会调用performReceiveLocked。这里区分两种情况:如果接收者是动态注册的,就直接通过IIntentReceiver的Binder接口调用performReceive;如果是静态注册的接收者且进程未运行,会先通过ProcessRecord启动进程,等进程起来后再投递。

对于动态接收者,AMS跨进程调用了InnerReceiver.performReceive,它内部通过ReceiverDispatcher.performReceive分发。这个类内部会创建一个Args对象,然后通过mActivityThread.post(args)把任务提交到指定Handler。注册时没传scheduler,默认用的是mMainThread.getHandler(),也就是主线程Handler。所以onReceive天然在主线程执行,这就是你可以在onReceive里安全更新UI,但不能做耗时操作的直接原因。

public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { ... final Args args = new Args(intent, resultCode, data, extras, ordered, sticky, sendingUser); if (intent == null || !mActivityThread.post(args)) { ... } }

对于静态接收者,如果进程还没启动,AMS会调用startProcessLocked创建进程。等ApplicationThread绑定完成,attachApplicationLocked里会继续处理该进程的pending广播。这就解释了为什么静态注册的广播能拉起一个冷启动的App进程。

进程起来后,投递走的是另一条路径:ApplicationThread.scheduleReceiver。

public void scheduleReceiver(Intent intent, ActivityInfo info, ResultInfo resultInfo, CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras, boolean sync, int sendingUser, int processState) { updateProcessState(processState, false); ReceiverData r = new ReceiverData(intent, resultCode, data, extras, resultInfo, sendingUser, info); ... sendMessage(H.RECEIVER, r); }

这里把ReceiverData作为消息发到ActivityThread的HHandler。主线程的HHandler收到了RECEIVER消息,就会调用handleReceiver。

4.2 handleReceiver与ClassLoader

handleReceiver这一步容易让人困惑,尤其是自定义BroadcastReceiver的动态加载问题。

private void handleReceiver(ReceiverData data) { ... LoadedApk packageInfo = mPackageInfo; ... IActivityManager mgr = ActivityManager.getService(); Application app; ... java.lang.ClassLoader cl = packageInfo.getClassLoader(); ... BroadcastReceiver receiver = (BroadcastReceiver) cl.loadClass(component).newInstance(); ... receiver.setPendingResult(data); receiver.onReceive(mInstrumentation.getTargetContext(), data.intent); ... }

看这段实现,onReceive不是AMS直接调用,而是ActivityThread在创建了BroadcastReceiver实例后,手动调用receiver.onReceive(ctx, intent)。这解释了静态注册接收者的生命周期:每次广播到达,ActivityThread都通过ClassLoader.loadClass创建一个新的BroadcastReceiver实例,调用onReceive,用完即弃。

这也带来一个开发注意事项:不要在BroadcastReceiver的成员变量里保存状态,因为每次广播都是新实例。如果你用静态registered的动态广播,同一个实例倒是可以复用,但线程安全要自己保证。

4.3 goAsync与PendingResult的关系

BroadcastReceiver继承BroadcastReceiver.PendingResult,onReceive返回时,如果是有序广播,系统会调用finish()来完成当前广播;如果是普通广播,onReceive返回后广播就算执行完了。

问题来了:onReceive里如果要执行耗时操作,比如起线程做网络请求,而onReceive已经返回了,那这个广播算结束了,接收者进程里的ActivityManager会标记这条广播已经处理完。对于有序广播,如果不做特殊处理,AMS可能继续投递下一条,你后边的异步结果还没回来,整个流程就乱了。此外普通广播虽然不要求finish,但耗时操作在onReceive里做,会导致主线程卡顿,可能直接触发ANR(Android 10里动态广播接收者onReceive超过超时时间同样会报ANR)。

goAsync就是解决这个问题的官方API。调用goAsync后,onReceive返回时广播不会被立即标记为完成,而是返回一个PendingResult对象,你可以在异步任务真正做完时调用pendingResult.finish()。

@Override public void onReceive(Context context, Intent intent) { final PendingResult pendingResult = goAsync(); new Thread(() -> { try { // 处理耗时逻辑 } finally { pendingResult.finish(); } }).start(); }

这块还有一个细节:PendingResult在调用finish()后,会通过ActivityManager.getService().finishReceiver(mToken, resultCode, resultData, resultExtras, resultAbort, flags)把结果回传给AMS。

4.4 finishReceiver如何回传AMS

finishReceiver是AMS中与投递对应的收尾动作。调用链是:App进程的PendingResult.finish()-> Binder -> AMS的finishReceiver->BroadcastQueue.finishReceiverLocked。finishReceiverLocked会从当前BroadcastRecord中移除当前接收者,处理结果码和abort标志,然后调用processNextBroadcastLocked进入下一条有序广播。

有序广播中,接收者如果调用了abortBroadcast(),实际上就是在PendingResult里设置了abort标志。finishReceiver回传AMS时,BroadcastRecord会检查r.resultAbort,为true就丢弃剩余接收者,整个广播分发结束。

对于有序广播超时,BroadcastQueue设置了broadcastTimeoutLocked,每投递一个接收者都会重新设置超时。前台广播10秒,后台广播60秒。超时后AMS会强制结束当前广播,同时把接收者的执行状态标记为超时ANR。源码里会把r.anr置为true,输出ANR日志,再继续后续广播。这就是为什么有序广播接收者里做耗时操作会拖垮整条有序链,甚至触发整个App的ANR。

5. API 29时代的广播约束与适配

5.1 隐式广播限制到了什么程度

从Android 8.0(API 26)限制后台服务的隐式广播,到Android 9.0(API 28)限制大部分隐式广播对静态注册接收者的投递,再到Android 10(API 29)的进一步收紧,静态注册的接收者能收到的隐式广播已经非常少了。

需要明确这里说的“隐式广播”是什么。如果发送的Intent没有指定setComponent或setPackage,只写了action,就是隐式广播。从API 28开始,非系统应用通过sendBroadcast发送的隐式广播,系统默认不会投递给Manifest里静态注册的接收者。这意味着只要你不是系统应用,也没有显式指定目标包名,静态广播收不到是版本策略下的预期行为,不是Bug。

那有哪些广播是静态注册仍然能收到的?系统发送的一些受豁免的广播,比如屏幕开关、开机完成、充电状态、时区变化这类系统广播仍然可以。另外,如果你的应用会发送显式广播,也就是intent.setPackage("com.example.target")或者setComponent(new ComponentName(...)),静态注册的接收者也能收到。

API 29的源码里,broadcastIntentLocked对显式广播和隐式广播的匹配路径做了区分。显式广播的intent.getComponent() != null,直接走组件解析,不经过mReceiverResolver的filter匹配。隐式广播会经过mReceiverResolver,然后在collectReceiverComponents查询静态接收者时,代码里有isAvailableForBroadcast和instantApp等检查,同时系统还会对静态注册的接收者做隐式广播豁免判断。

5.2 exported声明与安全检查

android:exported这个属性在API 29已经存在,但还没有强制所有含intent-filter的组件都必须声明。开发阶段提早适配没有坏处。原则上,不需要对外部应用暴露的广播接收者,exported设为false,避免外部应用向它发送恶意广播。

源码层面,AMS投递静态接收者时,会校验ActivityInfo.exported。如果接收者exported=false,而发送方不是系统应用,同时也不是同一个UID,AMS会直接跳过该接收者。这在动态广播里也一样,BroadcastFilter的权限校验逻辑里有一整套权限检查,包括发送方权限、调用者权限、requiredPermissions等。

5.3 广播之外的替代方案

后台限制越来越严之后,广播不再适合作为定期任务的调度手段。如果App里有“每隔一段时间做一次同步”的需求,推荐用JobScheduler或者WorkManager。WorkManager在API 29上会优先用JobScheduler来实现,能自动处理延迟、重试、系统电量优化等策略。而普通广播更适合做进程内的即时通知,比如登录成功、数据刷新、播放状态切换这类事件。

另一个值得提的是LocalBroadcastManager,它在API 29已经属于过时方案,官方推荐用LiveData、Flow或StateFlow替代应用的进程内事件传递。从机制上说,LocalBroadcastManager走的是进程内直接分发,不走Binder,也不经过AMS,所以速度快,但没有跨进程能力。如果你只要App内部模块通信,用LiveData或Flow比广播清晰得多,也几乎没有隐式广播收不到的问题。

6. 常见坑位与排查套路

6.1 收不到广播的原因排查清单

我整理了平时排查广播问题时的优先级顺序,逐条对照基本能定位问题。

  • 广播类型是否属于隐式广播且接收者是静态注册。API 29上非系统应用发送的隐式广播,静态接收者默认收不到。
  • 发送的Intent是否设置了包名或组件。如果设了setPackage但包名跟你接收者的包名不一致,也收不到。
  • receiver是否在Manifest里正确声明,且exported是否匹配。应用内接收建议exported=false。
  • 发送广播时是否带了权限,接收者selectPermission/注册时是否申请了该权限。
  • filter匹配规则:action、type、scheme、categories是否都匹配。这个是物理层面的匹配,不通过换什么版本都没用。
  • 动态注册是否在正确的context上注册,注册后context是否还活着。
  • 后台限制:App处于后台且没有白名单权限时,有序广播、后台广播可能被延迟。
  • 进程是否还在。动态注册的接收者只存在于注册进程存活期间。

6.2 动态注册泄漏与unregister的时机

动态注册最常见的问题是忘记反注册导致内存泄漏。registerReceiver里,AMS的ReceiverList会强引用IIntentReceiver,而IIntentReceiver的InnerReceiver虽然用WeakReference引用ReceiverDispatcher,但ReceiverDispatcher通常被LoadedApk保存,还未解除。真正的泄漏来源是:注册时传的scheduler默认是主线程Handler,ReceiverDispatcher持有Handler,Handler又间接持有Activity。如果Activity里注册了接收者但在onStop或onDestroy没反注册,Activity实例就会被ReceiverDispatcher的引用链拖住。

具体场景:在Activity的onCreate里注册了一个receiver,只在onDestroy里反注册。Activity重建时,如果用户从A页面跳到B页面,A没有及时销毁,receiver还在注册状态,就可能让A的实例一直存在。实测里最常见的泄漏路径是:Activity.onCreate注册,onDestroy里反注册代码因为异常被跳过。

所以建议在onStart/onStop这对生命周期里注册和反注册,而不是onCreate/onDestroy。配对的生命周期保证正常情况下一定会调用onStop,反注册的可靠性高很多。

6.3 有序广播导致的ANR与卡顿

有序广播的执行是串行的,每个接收者依次执行,而且不能在onReceive里做耗时操作。如果其中任何一个接收者超过超时时间(前台10秒、后台60秒),AMS就会报告ANR。API 29里,动态注册的接收者如果超时,同样会触发ANR,不只是静态注册的。

这里还有个小坑:goAsync虽然能延长执行时间,但延长同样受AMS超时约束,PendingResult的finish()必须在超时时间之内调用。如果finish()一直不调用,AMS还是会走broadcastTimeoutLocked,把广播强制结束。所以finally里调finish()是必须的,任何异常分支都不能漏。

6.4 广播顺序与进程优先级

普通广播的投递顺序不保证,这是设计如此,不是Bug。如果你想保证多个接收者之间的执行顺序,必须用sendOrderedBroadcast,并且精心匹配android:priority。动态注册的在setPriority高,Manifest里的priority只在系统广播的特定场景生效。

另外,广播接收者的进程优先级会影响系统是否会立即投递。如果接收者App在后台且进程已经被杀,AMS回调拉起进程时还要等进程启动完成,期间广播在队列里等待。如果同时有高优先级的前台进程需要接收同一条广播,后台接收者会被排到后面。

最后再聊聊实际体会

带团队做Android开发这几年,广播这块我踩过的坑不比业务逻辑少。最让我记忆犹新的是一次“静态广播收不到”的定位过程,查了半天代码,最后发现是targetSdkVersion切到了API 28以上,隐式广播直接被系统拦截了。从那以后我给自己养成了一个习惯:看广播相关的线上问题,先确认版本限制,再看注册方式,最后才看匹配逻辑。这个顺序能帮你省下大量排查时间。

如果你正要啃这套源码,我建议你从broadcastIntentLocked读起,它是整个广播机制的枢纽,把接收者收集、sticky处理、队列选择、权限校验全部串在一起。读通这个方法,再往上游看registerReceiver和sendBroadcast,往下游看BroadcastQueue和ApplicationThread,基本两天就能把广播机制的大框架搭起来。源码不难,真正难的是把这些调用的时序关系和安全约束转化成写代码时的肌肉记忆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询