ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android 动态注册 BroadcastReceiver 原理与实战:从 registerReceiver 到 AMS 完整链路

2026/9/9 15:51:34 拓冰建站 浏览量
Android 动态注册 BroadcastReceiver 原理与实战:从 registerReceiver 到 AMS 完整链路 做 Android 开发这些年广播BroadcastReceiver是我见过最容易被低估的组件。很多人写动态注册就是模板代码——registerReceiver写一次、unregisterReceiver写一次中间放个onReceive就完事。可一旦真的深挖问题一个接一个为什么同一个 Receiver 实例重复注册会崩为什么动态注册的 Receiver 在进程被杀后就不收广播而静态注册的还能收为什么 Android 14 之后动态注册突然强制要求带 exported 标志这些问题的答案全藏在“动态注册过程”这条主线上。这篇文章我准备从两个层面来讲动态注册第一层是 API 和实操保证你能直接拿去用第二层是 Framework 源码链路从ContextImpl.registerReceiver一路跟到 AMS再把广播发送回来时的调用路径串一遍。适合正在做系统开发、插件化方案或者准备 Framework 面试的同学也适合被“广播不触发”“Receiver 泄漏”这类问题折磨过的普通应用开发者。看完之后你会发现动态注册并不是一个孤立 API它是整个 Android 广播机制的入口搞懂它很多相关疑难杂症都会迎刃而解。1. 动态注册是什么为什么现在它是主流方案先给没接触过底层的人补个背景。Android 里的广播注册方式有两种一种是在AndroidManifest.xml里写receiver标签这叫静态注册另一种是代码里调用registerReceiver运行时注册、运行时注销这叫动态注册。静态注册从 Android 8.0API 26开始被大幅限制大量隐式广播不能再通过 Manifest 接收动态注册的地位一下就上来了。所以今天聊的整个过程不只是一个 API 的调用细节它直接决定你代码能不能收到广播、会不会泄漏、会不会踩版本兼容的坑。1.1 动态注册解决的核心问题动态注册最大的优势是生命周期可控。你可以把广播接收器绑定到一个组件的生命周期上Activity 可见时注册不可见时注销业务需要时注册不需要时立即注销。这让广播的接收窗口是完全可控的不会出现“界面已经销毁了还在收广播、白白唤醒一堆逻辑”的情况。另外一个核心价值是能绕开系统对隐式广播的限制。Android 8.0 之前你可以在 Manifest 里声明监听ACTION_PACKAGE_ADDED、ACTION_NEW_OUTGOING_CALL这类系统事件8.0 之后绝大多数隐式广播都不允许静态注册了系统只保留了一小部分白名单比如BOOT_COMPLETED、MY_PACKAGE_REPLACED、LOCALE_CHANGED等。而动态注册不受这个限制系统事件、应用间事件、应用内事件都能监听。这也是为什么现在大家看到的大部分网络切换、屏幕亮灭、电池变化监听都是动态注册实现的。从进程角度看动态注册还有一个隐性的“自负盈亏”特点注册者必须自身进程活着广播才能投递到它。进程一旦被杀AMS 会把它注册的 Receiver 信息一并清理掉。这和静态注册截然不同——静态注册的 Receiver 信息常驻 PMS/AMS进程不存在时系统会先拉起进程再投递广播。理解了这一点你就能明白为什么“动态注册收不到开机广播”是个伪命题开机时你的进程还没跑谁能替你注册呢1.2 动态注册的典型使用场景我按日常开发中遇到的高频场景大致分了几类系统状态监听网络连接变化、电量变化、屏幕亮灭、耳机插拔、时区切换、应用安装卸载。这类事件通常由系统或系统服务发出动态注册是最好的接收方式。应用内或模块间通信同一个 App 里不同模块之间解耦比如登录状态变化、播放器暂停通知、下载完成提醒。用广播做进程内通信虽然有点重但在没有引入事件总线或 Flow 的老项目里很常见。跨应用事件同步两个应用只要约定好 Action 和权限就可以通过广播互相通知。这种场景必须处理权限校验否则任何应用都能伪造广播投毒。系统能自动恢复的本地任务有些场景你想监听某个状态变化但只希望在前台时响应比如聊天界面在前台时才刷新消息未读数。这时候动态注册 生命周期绑定的组合比一直驻留一个静态注册 Receiver 省电得多。1.3 动态注册和静态注册应该怎么选很多新手会问“以后都用动态注册不就行了”不行。两种方式各有不可替代的场景。我整理过一张对比表方便在方案设计时直接参考维度动态注册静态注册注册入口代码registerReceiverManifest 中receiver生效时机注册后立即生效注销后立即失效安装后生效与应用进程生命周期无关生命周期跟随注册 Context 或手动注销跟随应用安装常驻进程被杀后不再接收系统清理注册信息广播到达时可拉起进程隐式广播限制Android 8.0 后仍可监听大部分Android 8.0 后仅白名单是否可动态注销可以不可以是否支持 intent-filter 优先级支持优先级更高支持优先级相对低典型场景前台业务监听开机启动、推送拉起、定时任务选型逻辑其实很简单如果你需要“进程死了也能被叫起来响应”就必须静态注册比如开机广播、设备管理员相关、消息推送的拉起逻辑如果只是应用活着的时候监听某个变化或者这个广播本身不是白名单里的隐式广播那就老老实实动态注册。强行静态注册一个被限制的隐式广播不会报错但广播到了你也收不到这种问题排查起来最折磨人。2. 从 registerReceiver 到 AMS 的完整链路拆解这一章是全文最核心的部分。动态注册表面上是“App 进程内注册一个接收器”实际上整个过程跨了进程App 调用registerReceiver最终是让系统进程里的 AMS 记录下这条注册信息。只有理解了这条链路你才能真正理解为什么 Receiver 不能重复注册、为什么回调一定在某条线程执行、为什么进程死了注册就没了。2.1 起点ContextImpl 的 registerReceiver 重载平时我们在 Activity 里直接写的registerReceiver(receiver, filter)最终都会走到ContextImpl。ContextImpl是Context的真正实现类里面提供了多个重载版本实际干活的方法可以简化为private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId, IntentFilter filter, String broadcastPermission, Handler scheduler, Context context, int flags) { 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); } try { // 最终 Binder 调用 AMS return ActivityManager.getService().registerReceiver( mMainThread.getApplicationThread(), mBasePackageName, rd, filter, broadcastPermission, userId, flags); } catch (RemoteException e) { return null; } } return null; }几个关键字值得解读。mPackageInfo在应用进程里通常是LoadedApk对象scheduler null时系统会默认把回调 Handler 设成主线程 Handler这就是为什么onReceive默认运行在主线程。你可能会问能不能自己传一个子线程 Handler可以registerReceiver有包含Handler参数的重载版本传进去之后回调就发生在指定 Handler 所在的 Looper 线程。但这种情况很少用因为广播回调本身设计成轻量级多数业务也依赖主线程做 UI 更新。调用链上还有一个值得注意的点registerReceiverInternal里拿到的是一个IIntentReceiver类型的rd而不是直接拿我们写的BroadcastReceiver对象传给 AMS。原因很简单BroadcastReceiver不是 Binder 对象不能跨进程传输。真正传给 AMS 的是它内部的 Binder 代理InnerReceiver。这一步是整个动态注册过程的关键转折点。2.2 关键对象ReceiverDispatcher 与 InnerReceiverLoadedApk.getReceiverDispatcher()的作用是给同一个BroadcastReceiver实例建立或复用一个ReceiverDispatcher。ReceiverDispatcher内部持有一个InnerReceiver extends IIntentReceiver.Stub这个InnerReceiver才是能跨进程传输的 Binder 对象。我直接用简化代码来看它做了什么public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r, Context context, Handler handler, Instrumentation instrumentation, boolean registered) { synchronized (mReceivers) { ReceiverDispatcher rd null; // 从缓存 Map 中根据 receiver 实例查找 // 如果找不到就 new 一个 ReceiverDispatcher if (rd null) { rd new ReceiverDispatcher(r, context, handler, instrumentation, registered); // 放入缓存 } return rd.getIIntentReceiver(); } }也就是说同一个BroadcastReceiver实例在同一个LoadedApk里只对应一个ReceiverDispatcher再对应一个InnerReceiver。这一点非常关键后面讲“同一个 Receiver 为什么不能重复注册”会用到。ReceiverDispatcher还承担了线程切换的职责。AMS 那一侧最终通过 Binder 回调InnerReceiver.performReceive()这个调用发生在 Binder 线程池里不能直接执行业务逻辑。ReceiverDispatcher内部会构造一个Runnable然后通过注册时指定的 Handler默认主线程 Handlerpost 到目标线程去执行最后再调用BroadcastReceiver.onReceive()。这也是为什么你永远可以在onReceive里直接更新 UI不用自己再切线程。顺带一提如果你用 LeakCanary 或者内存分析工具会看到泄漏对象往往指向ReceiverDispatcher因为它持有了BroadcastReceiver而BroadcastReceiver如果又被非静态内部类、匿名类持有外层 Activity 引用就会把整个 Activity 链挂在 Binder 上造成泄漏。后面排查章节会展开。2.3 AMS 侧注册数据的组织方式Binder 调用到达 AMS 的registerReceiver方法后系统会做几件事找到调用者的进程记录ProcessRecord和 UID用receiver.asBinder()作为 key去mRegisteredReceivers这个全局 HashMap 里查是否已有对应的ReceiverList没有就新建一个ReceiverList把IIntentReceiver、调用进程信息、UID、userId 都存进去接着创建一个BroadcastFilter把IntentFilter、权限信息、UID 等塞进去并加入ReceiverList最后把BroadcastFilter加到mReceiverResolver里供后续广播匹配使用。用大白话说AMS 维护了两张表一张是按 Binder 索引的“注册者列表”记录了每个回调对象属于哪个进程、哪个 UID另一张是IntentFilter解析器记录了所有注册者想接收哪些 Action。发送广播时AMS 会用Intent里的 Action 去mReceiverResolver里做匹配匹配到的结果再反查注册者信息确定该回调哪个进程、哪个 Binder 对象。这里有一个很直接的推论动态注册的信息是存在系统进程内存里的一个Binder对象引用 进程信息。如果应用进程被杀系统检测到 Binder 死亡会清理掉对应ReceiverList。所以“进程死后收不到动态注册广播”不是玄学是 AMS 的数据模型决定的。mRegisteredReceivers这个 HashMap 的 key 用的是receiver.asBinder()即IIntentReceiver对应的 Binder 节点。同一个应用进程里同一个BroadcastReceiver实例多次调用registerReceiver拿到的InnerReceiver是同一个 Binder 对象所以 AMS 只会往同一个ReceiverList里继续加BroadcastFilter。如果两次注册的 IntentFilter 覆盖了同一个 Action后注册的 filter 并不会覆盖之前的而是同时存在于列表里匹配时算两条记录。这会导致回调重复触发或注销时一次性清空多个 filter 的诡异行为。所以官方推荐的实践很简单一个 Receiver 实例只注册一次注销时再解绑。2.4 广播到达时的回调路径动态和静态有什么本质差异广播发出后AMS 会让BroadcastQueue去调度。针对不同的 Receiver 类型投递方式有本质区别动态注册的 ReceiverAMS 在注册时已经拿到了IIntentReceiverInnerReceiver这个回调对象。当广播要投递时如果目标进程存在AMS 通过IApplicationThread.scheduleRegisteredReceiver把 Intent 和这个回调对象送回应用进程应用进程的ApplicationThread再调用InnerReceiver.performReceiveReceiverDispatcher的Runnable被 post 到主线程 Handler最终执行BroadcastReceiver.onReceive。静态注册的 ReceiverAMS 通过IntentFilter匹配到的是 Manifest 里的ResolveInfo只知道目标组件名。如果目标进程不存在AMS 会先创建进程再通过IApplicationThread.scheduleReceiver通知应用进程去反射实例化 Manifest 中声明的 Receiver 类最后调用onReceive。一句话总结动态注册投递的是“回调对象”静态注册投递的是“类名”。这也是动态注册回调更快的原因之一少了一层组件启动和反射实例化的成本。从调度线程的角度看动态注册回调最终落在注册时指定的 Handler 线程默认就是主线程。而静态注册回调同样发生在主线程但前提是进程已经被拉起且主线程 Looper 已经运行。如果进程刚被拉起来做异步初始化广播回调可能会排到后面顺序不可控。3. 实操落地动态注册的标准写法和进阶细节原理讲完回到业务。下面这些代码我都实际用过也基于当前最新版本做了适配。第一部分是基础写法每个应用开发者都应该背下来第二部分是版本兼容特别是 Android 14 的 exported 标志第三部分是返回值和粘性广播的高级利用。3.1 一个完整的网络变化监听例子我先给一个标准模板代码。假设你要在进入某个页面时监听网络状态变化页面销毁后停止监听public class NetworkStateActivity extends AppCompatActivity { private static final String TAG NetworkState; private NetworkChangeReceiver mReceiver; private IntentFilter mFilter; Override protected void onStart() { super.onStart(); if (mReceiver null) { mReceiver new NetworkChangeReceiver(); } mFilter new IntentFilter(); mFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); registerNetworkReceiver(); } Override protected void onStop() { super.onStop(); unregisterNetworkReceiver(); } private void registerNetworkReceiver() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { mReceiver registerReceiver(mReceiver, mFilter, Context.RECEIVER_NOT_EXPORTED); } else { mReceiver registerReceiver(mReceiver, mFilter); } } private void unregisterNetworkReceiver() { if (mReceiver ! null) { unregisterReceiver(mReceiver); mReceiver null; } } private class NetworkChangeReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { ConnectivityManager cm (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); // 新项目建议用 NetworkCallback这里只是演示广播的用法 boolean connected intent.getBooleanExtra( ConnectivityManager.EXTRA_NO_CONNECTIVITY, false) false; Log.d(TAG, network connected: connected); } } } }有几个细节要强调。我把注册放在onStart、注销放在onStop而不是onCreate/onDestroy。这样用户退回桌面导致 Activity 进入 onStop 时接收器就注销了能减少无效回调。如果需要注册期间必须保障回调可以放 onResume/onPause。mReceiver registerReceiver(...)这个赋值不是必需的但这样写可以提醒自己registerReceiver有返回值而且可能是非空的 sticky Intent。后面细说。关于网络监听本身ConnectivityManager.CONNECTIVITY_ACTION在新版本上已经不再推荐系统更新后回调频率和触发时机都会变建议新项目直接用registerDefaultNetworkCallback。但这段代码用来演示广播的动态注册流程仍然是最清晰的入口。3.2 Android 14 带来的 exported 标志变化Android 14API 34开始动态注册 Receiver 必须显式声明“是否导出”。直接调用context.registerReceiver(receiver, filter)不带 flagstargetSdk 34 以上的应用会直接抛SecurityException。这是很多团队升级 targetSdk 后遇到的第一波崩溃甚至是线上崩溃。官方给的方式是显式传入两个标志之一// 表示当前接收器只接收同应用或同 UID 应用发出的广播安全级别较高 context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); // 表示当前接收器可以接收来自其他应用、甚至系统发来的广播 context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);RECEIVER_NOT_EXPORTED和RECEIVER_EXPORTED的区别可以类比为“只对自家人开放”和“对外公开”。对于纯应用内广播强烈建议用NOT_EXPORTED因为外部应用无法向你发送广播这对防止广播投毒、防止被动启动逻辑非常有用。如果你依赖了 AndroidX Core 库推荐直接用ContextCompat.registerReceiver它能帮你自动处理版本分支ContextCompat.registerReceiver( context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED );这里有一个容易踩的坑如果同一个动态注册的 Receiver 要接收多个 Action其中一部分 Action 只由系统发送另一部分由自家其他模块发送你可能需要评估改成两个 Receiver 实例分别用不同 exported 标志或者统一用RECEIVER_EXPORTED并自己校验发送包名和权限。千万别图省事全上EXPORTED尤其是收到广播后要拉起弹窗、跳转页面、读写敏感信息的逻辑。3.3 registerReceiver 返回值与粘性广播的利用动态注册这个方法有一个很容易被忽略的返回值Intent。它非空的场景只有一个——你注册的 Action 对应一个“粘性广播Sticky Broadcast”。什么叫粘性广播简单说就是系统会把最近一次广播的内容“粘住”保存起来。你后注册时系统直接把最近一次的内容返回给你即使这次广播已经发送过了。典型例子是ACTION_BATTERY_CHANGEDIntent stickyIntent registerReceiver( batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED), Context.RECEIVER_NOT_EXPORTED ); if (stickyIntent ! null) { int level stickyIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale stickyIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); int percent (int) (level * 100.0f / scale); // 注册后立刻拿到当前电量不需要等下一次电量变化 }这个特性很适合做“启动时读一次当前状态之后再持续监听变化”的场景。你用registerReceiver的返回值就能在回调到达前拿到现状避免界面初始状态空白。不过要提醒一句官方已经不建议自己发送粘性广播sendStickyBroadcast系列 API它在高版本上被标记废弃更多是系统内部事件在使用。我们作为普通应用开发者理解返回值即可不要在自己的业务里依赖 sticky 机制。3.4 进阶带权限注册、调度线程与 goAsync先说权限参数。动态注册的重载版本支持broadcastPermission参数context.registerReceiver( receiver, filter, com.example.permission.SAFE_BROADCAST, // 发送方必须持有该权限 null );这个参数的含义是一个应用要向你发送匹配该 filter 的广播它的 UID 必须持有com.example.permission.SAFE_BROADCAST这个权限。如果没有广播会被系统拒绝投递。这是跨应用通信时防止广播伪造的重要手段。注意它和静态注册里receiver android:permission...的语义不完全一样静态注册的android:permission约束的是发送方必须持有权限才能发送到该 Receiver动态注册这里的参数效果类似但粒度更细可以精确到某一次注册上。再说scheduler参数也就是 Handler。如果你确实希望某个 Receiver 的回调落到子线程可以自己传一个带子线程 Looper 的 Handler。但请务必想清楚onReceive里默认不保证线程安全同一个 Receiver 如果同时被注册到不同线程逻辑必须自己加锁。大多数场景没有必要主线程回调是设计默认值不要轻易破坏。最后是goAsync。onReceive不是普通方法它占用的是系统广播调度的一个执行槽。如果onReceive里做了耗时操作比如网络请求、数据库读写且没有及时返回系统会认为该接收器 ANR。正确姿势是调用goAsync()拿到PendingResult把任务放到子线程执行执行完必须调用finish()public class AsyncReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { final PendingResult result goAsync(); ThreadPoolExecutor executor Executors.newSingleThreadExecutor(); executor.execute(() - { try { // 执行耗时操作比如上报埋点、写数据库 } finally { result.finish(); } }); } }注意goAsync()必须在onReceive返回前调用且finish()必须在超时前调用否则系统照样按 ANR 处理。官方建议单个动态注册 Receiver 的生命周期尽量控制在 10 秒以内超时就会触发 ANR 弹窗。4. 常见问题与排查技巧实录这一章是我在项目里踩坑最多的部分。很多问题说起来都是小细节但一旦线上出现排查路径绕来绕去很费时间。下面按“报错现象”到“根因”的方式整理你可以直接当速查表用。4.1 重复注册、重复注销导致的崩溃最经典的一个崩溃java.lang.IllegalArgumentException: Receiver not registered: ...原因通常是同一个 Receiver 实例被调用了两次unregisterReceiver或者根本没有注册成功就注销。还有一种隐藏更深的情况你在onStart注册但上一次onStop注销时把mReceiver置空了下一次进入onStart重新 new 了一个 Receiver逻辑上没问题但如果你的注销代码没有做空判断或者注册时传入的是同一个单例 Receiver多走了一次注销就崩。排查建议注销前用try-catch包一层不是好习惯但也不是不行关键是先保证注册和注销成对出现。在注册和注销的入口打日志把堆栈打出来定位是谁多调了一次。结合 2.2 节的原理同一个BroadcastReceiver实例在LoadedApk里只对应一个ReceiverDispatcherAMS 里也只对应一个ReceiverList。unregisterReceiver是根据这个 Binder 对象去移除对应ReceiverList已经移除过了再移除自然抛异常。所以尽量避免“同一个 Receiver 实例被多个页面共享注册”的设计除非你用引用计数精细管理。4.2 回调不触发隐式广播限制与拦截原因如果你发现动态注册了但onReceive一直没走按下面顺序排查确认 Action 是否拼写完全一致。多一个点、少一条斜线都匹配不上。确认注册的 IntentFilter 是否包含你要监听的 Action。有的同学注册了一个空 filter然后intent.setAction动态拼接这通常没问题但如果你 filter 里写的是自定义常量发送端和接收端必须引用同一个类最好放到公共模块。确认发送方式。发送方如果用了setPackage(packageName)指定包名只有该包名下的匹配 Receiver 能收到。动态注册的进程如果不是目标包名之一收不到。确认发送方和接收方的权限匹配。发送方用了setPermission(perm)接收方注册时没有匹配 permission广播会被拦截。确认目标组件是否是COMPONENT_ENABLED_STATE_DISABLED或者应用被系统挂起。确认你注册的 Context 是否已经失效。比如你在一个已经 destroy 的 Activity 里注册但一直没注销Activity 仍持有 Receiver理论上还能收到但如果你用getApplicationContext()注册然后 Activity finish 后忘记注销Receiver 会收到但内部持有的 Activity 已经销毁逻辑照样出错。再强调一次 Android 8.0 之后的隐式广播限制虽然动态注册普遍能绕过但不是所有静态注册都失效也不是所有动态注册都能收到系统广播。对于某些系统限制广播比如ACTION_PACKAGE_CHANGED的某些细分场景系统可能只投递给静态注册的 Receiver或者只投递给系统应用。遇到系统广播不触发时先查官方文档的事件列表确认该事件对普通应用是否开放。4.3 动态注册导致的内存泄漏动态注册不注销是 Android 内存泄漏的重灾区之一。前面说过AMS 侧持有的是InnerReceiverBinder应用进程侧ReceiverDispatcher又强引用BroadcastReceiver。如果BroadcastReceiver是 Activity 的非静态内部类或匿名类它就隐式持有外层 Activity 引用。一旦注册了没注销即使 Activity 已经 finish这个引用链是断不掉的。LeakCanary 的检测结果往往长这样* com.example.MainActivity * BroadcastReceiver * ReceiverDispatcher * InnerReceiver * IIntentReceiver怀疑是动态注册泄漏时按下面几步处理看 Receiver 类是不是静态内部类或独立类。匿名内部类 Activity 直接作为 Receiver 的写法忘注销基本必泄漏。如果 Receiver 需要访问 Activity用静态内部类 WeakReferenceActivity的组合。按组件生命周期严格配对注册和注销。不确定在哪个生命周期注册合适时默认onStart/onStop配对比onCreate/onDestroy更安全因为 onStop 在退到后台也会触发。用ApplicationContext注册的 Receiver 不随 Activity 销毁这是正确的用法但 Receiver 内部绝不能持有 Activity 引用否则照样泄漏。我在新项目里的默认模板是Receiver 独立成静态类通过接口或者回调把事件传给外层组件即使某个页面忘记注销也不至于把整个 Activity 挂在广播链路上。4.4 进程与多进程场景下的动态注册动态注册的行为是“按进程隔离”的。如果你的 App 开启了多进程比如:remote、:push这类 process那每个进程的LoadedApk是独立的registerReceiver也是各自向 AMS 注册。一个进程里注册的 Receiver收不到另一个进程发出的广播。如果你期望跨进程通信两个进程都注册同一个 Action并且发送方明确指定包名是能收到的但不要在 A 进程注册后指望 B 进程也能收到。还有一种情况同一进程内你用两个不同的 Context 注册同一个 Receiver 实例。由于LoadedApk.getReceiverDispatcher的缓存 key 其实混合了 Receiver 实例和 Context 信息两个 Context 会产生两个ReceiverDispatcherAMS 里会有两个ReceiverList。注销时如果只注销其中一个另一个仍然收广播。这种设计很容易造成“注销了还在回调”的幻觉。建议一个 Receiver 实例只绑定一个 Context 生命周期。多进程场景下还有一个坑如果接收广播的进程是关键进程比如主进程被系统杀死它的动态注册信息会被清掉。即使另一个进程还活着也无法替主进程接收并处理广播。所以进程通信、消息通知这类任务不要把核心逻辑压在动态注册上最好配合前台服务或 JobScheduler 保证主进程存活策略。4.5 性能问题别让动态注册变成“广播风暴”最后说一个偏性能的实践。动态注册虽然好用但同一时刻注册太多的 Receiver会影响 AMS 广播匹配和投递的整体效率。尤其在系统发送全局广播比如时间变化、网络变化时每个匹配的 Receiver 都会收到一次回调。如果你的应用里有几十个页面各自注册了同一个系统 Action 的 Receiver哪怕只有一个页面在前台其他后台 Activity 如果没有正确注销也会被唤醒做无效处理。这里有一个工程上的建议全局只需要一份系统事件监听的地方把它收敛到单一入口比如一个Application级别的单例监听器内部再用回调分发到需要的地方。不要在每一个页面上都注册系统广播。自定义业务广播尽量带上包名限制或权限限制减少无关应用被唤醒。在后台时注销与 UI 强相关的 Receiver只保留必要的业务监听也是一种省电和减负手段。把“按需注册、及时注销”当成一条纪律而不是可有可无的习惯。很多线上耗电和卡顿问题最后定位到根因就是后台无意义的广播回调太多。结合上面这些排查经验我自己后来固定下来的写法是页面级广播统一在onStart/onStop配对注册注销应用级系统事件监听放在单例中用ApplicationContext注册并且只在需要时开启所有自定义广播都加权限校验或包名校验Android 14 之后统一走ContextCompat.registerReceiver用一个工具类包装起来避免每个页面重复写版本判断。这套规矩看着简单但在团队协作时能省下大量排查时间。动态注册这条链路从表面看只是一个 API 调用往下挖其实牵涉 Android 的跨进程 Binder 通信、系统服务的对象管理、应用进程的生命周期、主线程调度值得每个做 Android 的人认真理一遍。掌握了它后面再看广播发送流程、静态注册、AMS 的 BroadcastQueue 调度都会顺很多。