首页 / 资讯中心 / 文章详情

Android广播机制源码解析:从注册到接收的完整调用链

Android广播机制源码解析:从注册到接收的完整调用链 ★ FEATURED ARTICLE
开头部分得先拉个远景。很多人学Android广播查了大量博客看到一堆注册、发送、接收的代码但始终没搞清楚这些代码在执行时到底经过了哪些系统进程、哪些Binder调用、哪些消息队列。光知道registerReceiver和sendBroadcast怎么用遇到“收不到广播”“顺序错乱”“进程被杀”这种线上问题照样抓瞎。这篇文章基于Android 10API 29的AOSP源码把动态广播从注册、发送到接收的完整调用链逐层拆开把关键的类、方法、参数都点到位适合刚接触源码的初级开发也适合需要排查广播疑难杂症的中级开发。看完你会明白广播这条链路本质上就是一次跨进程调用加一次消息调度把它当成一条流水线去理解源码就没那么难啃了。1. 广播机制全景注册、发送、接收到底是怎么串起来的1.1 系统进程与App进程的分工模型Android的广播机制是个典型的多进程协作场景。发送方App调用sendBroadcast接收方App通过registerReceiver或者Manifest静态注册表达“我想听什么广播”。这两个App进程本身不直接通信所有广播都汇入系统进程system_server里的ActivityManagerService由它做匹配、排队、分发。接收方的进程可能还活着也可能没起来AMS要根据情况决定是直接投递到已运行的进程还是先把接收者拉起来再投。理解广播的关键是先理解这三方各自的职责发送方只负责把Intent塞给AMSAMS负责拿这个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 10API 29的广播相关源码处在一个比较有代表性的节点它已经继承了Android 8.0后台执行限制、Android 9.0隐式广播限制又比Android 12之后动辄强制的exported声明宽松不少代码层面BroadcastQueue、BroadcastDispatcher等核心类也相对稳定。拿这个版本啃源码既能看清机制的全貌又不至于被最新的各种兼容策略干扰理解。2. 注册过程源码全链路从Context到AMS2.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 WeakReferenceReceiverDispatcher 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往往又持有ContextContext又可能持有Activity。如果InnerReceiver强引用ReceiverDispatcherAMS的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广播注册后立刻能收到一次。IteratorString actions filter.actionsIterator(); if (actions ! null) { while (actions.hasNext()) { String action actions.next(); stickies mStickyBroadcasts.get(userId); if (stickies ! null) { ArrayListIntent list stickies.get(action); if (list ! null) { if (intent null) { intent list.get(0); } ... } } } }第三注册接收者。AMS把每个动态注册的接收者封装成ReceiverList存入mRegisteredReceiverskey是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 IntentAMS会调用deliverToRegisteredReceiverLocked把广播直接投递给刚注册的接收者。这个投递其实是走了与正常发广播相同的分发路径只是接收者列表就是当前这个新注册的接收者。2.4 sticky广播的注册逻辑到底谁在“记住”广播我单独把sticky广播拎出来讲因为它很容易踩坑。sendStickyBroadcast在API 21以后已经不推荐使用但源码层面它仍然存在。sticky广播发送时AMS在broadcastIntentLocked里会把Intent保存到mStickyBroadcasts以userId为keyaction为内层key存储的是一个ArrayListIntent。后续注册接收者时如果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查询得到ListBroadcastFilter静态注册的接收者通过collectReceiverComponents查询得到ListResolveInfo。ListObject receivers null; ListBroadcastFilter 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为trueAMS会把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继承IntentResolverqueryIntent会根据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与ClassLoaderhandleReceiver这一步容易让人困惑尤其是自定义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.PendingResultonReceive返回时如果是有序广播系统会调用finish()来完成当前广播如果是普通广播onReceive返回后广播就算执行完了。问题来了onReceive里如果要执行耗时操作比如起线程做网络请求而onReceive已经返回了那这个广播算结束了接收者进程里的ActivityManager会标记这条广播已经处理完。对于有序广播如果不做特殊处理AMS可能继续投递下一条你后边的异步结果还没回来整个流程就乱了。此外普通广播虽然不要求finish但耗时操作在onReceive里做会导致主线程卡顿可能直接触发ANRAndroid 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如何回传AMSfinishReceiver是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.0API 26限制后台服务的隐式广播到Android 9.0API 28限制大部分隐式广播对静态注册接收者的投递再到Android 10API 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。如果接收者exportedfalse而发送方不是系统应用同时也不是同一个UIDAMS会直接跳过该接收者。这在动态广播里也一样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是否匹配。应用内接收建议exportedfalse。发送广播时是否带了权限接收者selectPermission/注册时是否申请了该权限。filter匹配规则action、type、scheme、categories是否都匹配。这个是物理层面的匹配不通过换什么版本都没用。动态注册是否在正确的context上注册注册后context是否还活着。后台限制App处于后台且没有白名单权限时有序广播、后台广播可能被延迟。进程是否还在。动态注册的接收者只存在于注册进程存活期间。6.2 动态注册泄漏与unregister的时机动态注册最常见的问题是忘记反注册导致内存泄漏。registerReceiver里AMS的ReceiverList会强引用IIntentReceiver而IIntentReceiver的InnerReceiver虽然用WeakReference引用ReceiverDispatcher但ReceiverDispatcher通常被LoadedApk保存还未解除。真正的泄漏来源是注册时传的scheduler默认是主线程HandlerReceiverDispatcher持有HandlerHandler又间接持有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基本两天就能把广播机制的大框架搭起来。源码不难真正难的是把这些调用的时序关系和安全约束转化成写代码时的肌肉记忆。
阅读完成 · 觉得有帮助?
咨询建站