
做Android开发这几年我见过太多人写广播都是照着模板抄sendBroadcast一调registerReceiver一注册onReceive里打条日志本地跑一下能通就以为理解了广播的发送和接收过程。结果一到真机联调就翻车——发送方明明调了sendBroadcast接收方就是没动静应用退到后台动态注册的广播直接失联Android 8.0之后清单文件里配好的静态广播收到次数永远是0。这些问题的根子几乎都在于只背了用法没吃透广播背后那条完整链路从sendBroadcast按下的一瞬间到Receiver.onReceive被回调系统里到底发生了什么、做了哪些决策、你在哪一步被系统规则拦截了。这篇文章就把这条链路完整拆开讲明白。我会按接收端怎么登记、发送端怎么投递、系统中间层怎么做匹配、版本升级后加了哪些门禁、出了问题怎么定位的顺序来写适合正在写广播功能、或者排查广播收不到问题的Android开发者也适合刚接触系统源码、想搞懂AMS和Binder机制的同学。读完你至少能回答三个问题广播到底是怎么从A进程到B进程的为什么有的广播静态注册收不到收不到时第一眼该查哪里。1. 广播是个三方协作模型别再把它当成方法回调1.1 邮局分拣模型发送方不直接联系接收方理解广播最好的方式是把它想象成邮政系统。你sendBroadcast(intent)只相当于把一封信投进邮筒。寄件人是你收件人理论上可以是任何注册了我想收这类信的组件。信能不能到收件人手里不取决于你认识多少收件人而取决于中间的分拣中心——Android系统服务里有一套专门的广播管理中枢。这套中枢会取出信封上的action、category、data、type、包名等信息跟每个收件人预先登记的兴趣清单IntentFilter逐一比对然后决定把这封信投到哪些进程、哪些Receiver去。这里有两个容易被忽略的细节。第一发送方和接收方不一定在同一个进程。跨应用广播的场景里接收方进程甚至可能处于完全没启动的状态。这种场景下方法直调根本做不到广播机制存在的核心意义就是跨进程、跨生命周期地传递事件。同一个应用内部你当然可以用回调、用Flow但跨应用只能靠这个邮局。第二系统的分拣中心不是无限并发干活的。它把所有待办广播装进队列按前台/后台、有序/无序做分级调度。这也是为什么你在onReceive里拖太久会ANR——因为分拣员在等这个收件人签收你不能让他一直卡着。1.2 为什么不能直接用回调或EventBus替代广播很多人问我项目里用EventBus也能解耦为什么还要广播我的回答是看场景。如果你的发送方和接收方在同一个进程、生命周期也受控用回调、LiveData、Flow都更好——API简单、调试方便、没有系统调度开销。但要跨应用通知第三方App或者实现应用进程没启动也能被唤醒这种需求只有系统级广播能做到。广播最不可替代的价值有两层。一层是监听系统级事件源开机完成、网络切换、电量变化、应用安装卸载这些都是系统进程在发广播你想监听就只能注册广播接收器任何进程内的事件总线都替代不了。另一层是拉起能力的差别EventBus能把事件分发给已经存在的对象但无法把一个被杀掉的进程拉起来。静态注册的广播可以做到——即使接收方进程不存在系统也会先把应用进程启动再实例化Receiver然后回调onReceive。这个能拉起进程的能力是双刃剑它给了你机会也给了系统限制你的理由后面第四章会详细说。当然这种能力有代价广播Intent要经过系统服务中转跨Binder传递整体过程比进程内回调慢几个数量级。而且系统的调度时间是有限的像onReceive就有一条悬在头顶的超时红线。在单进程、高频、实时性要求高的场景里硬用广播属于用大炮打蚊子。1.3 两种注册方式与四类广播的基本盘先把基本盘盘清楚。注册方式就两种动态注册代码里registerReceiver(receiver, filter)。接收者跟随组件生命周期一般写在Activity或Fragment里进程死了注册就没了。静态注册AndroidManifest.xml里声明receiver。由系统在安装时解析并保存应用没启动也能收到广播并拉起进程。广播按发送方式常见四类类型调用方式特点普通广播sendBroadcast异步、无序所有匹配的接收者最终都能收到有序广播sendOrderedBroadcast按优先级逐个分发前面的接收者可以截断粘性广播sendStickyBroadcast历史上用于保存最新状态早已废弃新代码别碰系统广播由系统进程发出应用一般只能收不能发这里要特别纠正一个常见误解普通广播是无序的并不意味着并行。很多人以为sendBroadcast发出去之后所有Receiver会同时收到其实系统依然是一条条顺序投递的只是接收者之间没有先后顺序保证、也不能互相截断而已。把无序理解成随机顺序投递才是对的。2. 接收端链路动态注册和静态注册走的其实是两条路2.1 动态注册registerReceiver时系统替你保存了什么先看最常见的Activity里注册BroadcastReceiver receiver new MyReceiver(); IntentFilter filter new IntentFilter(com.example.MY_ACTION); context.registerReceiver(receiver, filter);这行代码最终会走到ContextImpl.registerReceiverInternal。这里会做一件非常关键的事创建一个ReceiverDispatcher里面包着一个Binder对象InnerReceiver。为什么需要Binder对象因为应用进程和系统进程AMS之间是跨进程的AMS没法直接调用你的Receiver必须有一个能跨进程通信的代理。InnerReceiver就是那座桥。AMS收到注册请求后会把InnerReceiver相关的信息注册到自己的数据结构里ReceiverList记录当前应用进程里注册了哪些Receiver对应到LoadedApk里的ReceiverDispatcher。IntentResolver按action、category、scheme等维度建立索引方便后续发送广播时快速匹配。同时还有一个权限校验环节AMS会检查注册者有没有权限接收某些受保护的系统广播。没权限的注册请求会在这一步被拒绝。所以动态注册的本质是你在系统进程里登记了一行我想收什么信、送到哪个门的表单。表单什么时候失效两种可能一是你主动unregisterReceiver二是注册所在的进程被杀死。注意是进程被杀死不是Activity销毁——很多同学在onDestroy里忘了unregisterReceiverActivity虽然销毁了但进程还活着Receiver也就一直活着这就是经典的内存泄漏来源。2.2 静态注册没进程也能被叫醒的背后机制静态注册的入口是清单文件receiver android:name.BootReceiver android:exportedtrue intent-filter action android:nameandroid.intent.action.BOOT_COMPLETED / /intent-filter /receiver应用安装时PackageManagerServicePMS在解析APK时会扫描所有receiver节点把ReceiverInfo连同IntentFilter保存起来。注意这个阶段接收者的类还没有被实例化只是登记在册。当系统发送一个广播AMS在解析匹配接收者时除了从动态注册的mReceiverResolver里查还会向存储在PMS里的静态注册表查询匹配项。如果匹配到某个静态接收者组件而且它所在进程还没启动AMS就会通过startProcessLocked把进程拉起来。进程起来后ActivityThread完成初始化系统再通过scheduleReceiver把广播信息投递到应用进程handleReceiver里用反射创建BroadcastReceiver实例最后调用onReceive。这里有个关键区别动态注册的onReceive是已存在进程里的回调静态注册的onReceive则多了一次进程冷启动的完整流程。所以静态广播第一次回调的耗时往往比动态广播高很多这也是为什么很多启动广播处理不当会导致整个App启动被明显拖慢。你要是没做过性能分析可能会以为是Application里初始化慢其实卡点可能在等广播分发。2.3 onReceive的执行线程、上下文与耗时红线无论动态注册还是静态注册onReceive默认都执行在应用主线程UI线程。这一点很多人嘴上知道但写代码时还是忘。静态注册的Receiver实例是一次性的——系统在分发广播时创建实例onReceive执行完之后这个实例就可以被回收。动态注册的Receiver实例是持久的从注册到取消注册之间一直存活所以动态Receiver里可以持有一些状态但要注意线程安全。onReceive里能干什么限制其实很明确不要做耗时操作。官方文档的建议是onReceive里不要做太多工作必要场景下应该交给WorkManager或JobScheduler。不要滥用goAsync()。goAsync()确实可以把异步工作放到别的线程但系统仍然在等PendingResult完成总的时间限制并没有消失。不要在里面直接startActivity去弹复杂页面Android对后台启动Activity有严格限制广播接收器里弹窗翻车率极高。超时红线具体是多少前台广播要求10秒内完事后台广播相对宽松到60秒。但别以为60秒就能乱来因为后台进程随时可能被系统以各种理由杀掉超时只是其中一种死法。我在实际项目里见过有人在onReceive里做网络请求用goAsync续命结果两分钟都没回来最后进程被系统回收连日志都没留下。3. 发送端链路sendBroadcast之后系统才刚开始工作3.1 从ContextImpl到AMS发送动作的第一跳看完接收端再把发送端的链路走一遍。调用sendBroadcast后实际工作会从ContextImpl开始最终通过Binder进入AMS的broadcastIntent。从用户代码到AMS这里就已经跨了一次进程。AMS的broadcastIntentLocked里会依次做这几件事解析Intent参数取出component、action、data、type、flags、package等字段。权限校验检查发送者是否有权限发送这个广播。某些受系统保护的action非系统应用直接发送会被拦截。解析接收者同时从动态注册表和PMS静态注册表里查出所有匹配的Receiver集合。封装成BroadcastRecord放入BroadcastQueue队列等待逐条分发。这里有一个很重要的认知广播发送不会立刻回调而是先进队列由队列线程按规则取出来处理。AMS里根据广播的种类会创建不同的队列比如前台广播队列和后台广播队列分别对应不同的超时时间。所以你在sendBroadcast那行代码后面立刻做事情完全不能保证接收方已经收到了因为中间隔着一个异步调度系统。3.2 匹配决策Action、Category、Data和包名如何共同影响投递很多人广播收不到第一个怀疑的就是权限其实大部分情况下问题出在Intent匹配。IntentFilter匹配不是只比较action一个字段一个完整的匹配要过好几关actionIntent里的action必须和IntentFilter里声明的某个action一字不差。categoryIntent里携带的category必须全部在IntentFilter里出现过多一个都不行。dataschema、host、port、path、mimeType这些都会被拿出来比对。举个例子你发送的时候这么写Intent intent new Intent(com.example.MY_ACTION); intent.setData(Uri.parse(https://example.com)); sendBroadcast(intent);接收方Filter里如果只写了action没写data相关约束那么这个广播是匹配不上的因为系统会把data也纳入匹配范围。反过来也一样你发送时不带dataFilter里却要求了data也匹配不上。这是非常隐蔽的坑因为它编译期不会报错运行时也不会有异常就是静默地收不到。还有一个容易被忽视的点flags和package。Intent.setPackage(com.xxx)会让广播变成定向广播只投递给指定包名的应用Intent.setComponent()则直接指定组件连匹配环节都省了。很多人写的时候不自觉地加了包名又怀疑为什么别的应用收不到其实是被包名锁死了。3.3 有序广播的分发与截断动态和静态的优先级真相有序广播用sendOrderedBroadcast(intent, null)发送系统会按优先级从高到低逐个分发。优先级来源有两个动态注册的IntentFilter.setPriority()以及静态清单里的android:priority属性。我要重点说一个很多人不知道的细节在有序广播分发里所有动态注册的接收者默认排在整个静态注册接收者列表的前面这不是单纯由priority决定而是AMS在收集接收者时就把两组分开放了。动态的查出来放在前面静态的放在后面然后两组内部各自按照priority排序。所以就算你给静态Receiver配了999的priority它还是得排在动态Receiver后面。有序广播给了接收者两个控制手段abortBroadcast()可以截断广播后续接收者全部收不到setResultCode()和setResultData()可以向下游传递数据后面的接收者用getResultCode()和getResultData()读取上一步的结果。// 接收者A收集到结果后传给下游 onReceive(Context context, Intent intent) { setResultCode(Activity.RESULT_OK); setResultData(from A); // 不调用 abortBroadcast()继续传递 } // 接收者B读取上游结果 int code getResultCode(); String data getResultData();这种上下游协作在跨应用场景里非常好用比如主应用发一个采集设备状态的广播插件A往结果里塞状态主应用再读回来。但要注意普通广播没有这套机制你在普通广播里调setResultCode是无效的因为普通广播根本没有维护这批结果数据。4. 版本演进留下的特殊限制不改就会踩的坑4.1 Android 8.0的隐式广播限制与包名豁免细节Android 8.0API 26开始清单文件里静态注册的Receiver不再能接收绝大多数隐式广播。所谓隐式广播就是没有指定具体组件、只靠action让系统做匹配的广播。你的App在清单里写了action android:namecom.example.CUSTOM_ACTION/发送方发sendBroadcast(new Intent(com.example.CUSTOM_ACTION))targetSdk 26以上的应用里这套静态注册是收不到的。但有两个豁免细节很多人不知道系统有自己的白名单。开机广播、网络变化、飞行模式等部分系统广播仍然允许静态注册接收这就是为什么很多App的启动联动功能在8.0之后还能勉强工作。包名指定可以绕过限制。如果Intent调用setPackage(com.xxx)指定了包名那么静态注册的Receiver依然能收到这个广播。因为带包名的Intent在语义上已经是定向给某个包了系统会把这种情况当作显式广播来处理。所以静态注册收不到不是绝对的正确说法是静态注册收不到纯action匹配的隐式广播。在写跨进程自定义广播时最稳的姿势就是动态注册或者发送时用包名做显式化。4.2 动态注册的导出标志要求与系统广播的特殊性Android 14API 34开始如果你的targetSdk是34或更高动态注册Receiver时必须有导出标志。直接调用旧的context.registerReceiver(receiver, filter)只要接收的不是系统广播就会抛SecurityException应用直接崩溃。正确的写法是ContextCompat.registerReceiver( context, receiver, filter, ContextCompat.RECEIVER_EXPORTED );或者ContextCompat.registerReceiver( context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED );RECEIVER_EXPORTED表示其他应用可以给你发广播RECEIVER_NOT_EXPORTED表示只接收系统广播和自己应用发送的广播。这两个标志如果选错了功能上会有明显差别你用NOT_EXPORTED注册了一个第三方的自定义广播会发现怎么都收不到你用EXPORTED注册接收系统权限类广播又会带来不必要的安全暴露面。这里多说一句很多同学升级targetSdk 34之后遇到的第一批闪退就发生在这。平时开发时targetSdk还停留在旧版本registerReceiver不带flag一直没报错一上架或者测试机升级就成了定时炸弹非常值得提前自查。4.3 自定义Action别碰系统前缀也别指望它能跨应用拉起进程自定义广播的Action命名我有三条建议不要用android.intent.action.*开头。这个前缀是系统保留的自定义Action用它容易跟系统广播混在一起轻则匹配混乱重则被安全策略拦下。自定义Action一定要带自己应用的包名前缀比如com.example.app.ACTION_REFRESH。这样既能避免冲突排查日志时也能一眼看出来源。跨应用的自定义广播能走动态注册就别走静态注册。静态注册那条路在8.0之后已经被堵得很窄了除非你做的是系统白名单里的那几种场景否则不要把业务逻辑押在静态注册上。还有一个常见的错误预期以为发广播就能把一个在后台被杀掉的应用拉起。实际上动态注册的Receiver进程都没了肯定收不到静态注册的被限制以后自定义action基本也收不到。所以在设计架构时永远不要假设发一个广播对方App就会起来干活。真要跨应用传数据Android推荐用ContentProvider配合服务或WorkManager要通知另一个应用系统有专门的PendingIntent机制。广播适合的是事件通知不是任务调度。5. 广播收不到的排查实战从logcat到dumpsys5.1 按发送→匹配→分发→执行四步定位遇到广播收不到别急着改代码按这个顺序一步步排查能省下大量时间。第一步确认发送端真的发了。别笑我经常遇到开发者的发送代码包在一个永远走不到的分支里或者被异常吞掉了。最简单的办法是在sendBroadcast前后各打一条日志确认执行到了、参数没传错。第二步确认接收端注册成功。动态注册要看注册代码是否真的执行了注册时机对不对——有些同事把注册写在onCreate里Activity因配置变更重建后旧Receiver没注销新Receiver注册又失败乱成一团。静态注册要看清单文件有没有解析到android:exported的值是否符合预期。第三步确认系统匹配没问题。这是最隐蔽的一环。把发送端和接收端的action、category、data对照着看一遍尤其注意data和type这种容易被忽略的字段。跨应用广播还要确认有没有包名定向。第四步确认分发环节。如果前面都正常就要看接收方进程是否被杀、有没有ANR记录、发送方有没有权限被拦。系统日志里通常会有蛛丝马迹。5.2 高频坑位清单我整理了一个日常排查时最常用的对照表每一条都是实际踩过的现象常见原因正确姿势静态注册的隐式自定义广播收不到Android 8.0隐式广播限制改用动态注册或发送时setPackage显式化动态注册在Activity销毁后仍有回调onDestroy里没unregisterReceiver成对注册注销生命周期结束前注销广播在主线程只能做10秒内的操作超时红线onReceive里只做轻量操作耗时任务交给WorkManagerAndroid 14上动态注册直接崩溃未指定导出标志使用ContextCompat.registerReceiver加RECEIVER_EXPORTED/NOT_EXPORTED发送带data的广播接收方收不到IntentFilter的data匹配不一致检查schema、host、path是否完全对齐或去掉data依赖有序广播中下游收不到上游结果上游调了abortBroadcast检查上游逻辑abortBroadcast慎用同一个Action动态能收到静态收不到静态注册受隐式广播限制统一走动态注册方案5.3 我实际用过的排查命令与日志姿势命令行排查是广播问题定位的加速器。我最常用的两条命令# 查看系统里当前注册的所有广播接收者 adb shell dumpsys activity broadcasts # 手动发送一条测试广播 adb shell am broadcast -a com.example.test.ACTIONdumpsys activity broadcasts的输出里能看到两类关键信息一类是当前动态注册的接收者列表会列出Action、Receiver的类名、进程名可以用来确认系统认为你注册了吗另一类是最近广播的历史记录能看到AMS刚才处理过的广播排队痕迹。如果广播根本没被系统看到那问题多半出在发送端或权限层。日志姿势上我强烈建议给onReceive的第一行加这样一个日志Log.d(ReceiverTag, onReceive called on thread Thread.currentThread().getName() , action intent.getAction());加上线程名能看到回调是不是真的跑在主线程加上Action能立刻确认你收到的是不是你期望的那条广播。很多收不到其实不是没收到而是收到了别的Action或者同一个Action被过滤了多次。如果要做Intent解析的细粒度调试可以在发送方Intent上加上调试flagintent.setFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);加了之后系统会在日志里输出更详细的解析和匹配过程对排查隐式Intent匹配问题很有帮助。但要注意别在正式版本里留着这个flag日志量比较大。最后分享一个经验层面的小技巧把项目里所有自定义Action统一集中到一个常量类里管理应用包名做前缀。我见过太多同事因为字符串不匹配排查了一下午最后发现就是Action里一个下划线写错了。集中管理加上发送前打日志基本能把广播问题从玄学变成科学。广播这条链路其实不复杂它只是把进程间通信的复杂性藏在系统中间层后面了。一旦你把发送、注册、匹配、分发这几段解释清楚了再回头看sendBroadcast那个调用你会明显感觉到按下邮筒投信口的一瞬间后面有一整套系统在帮你跑腿。