
1. 为什么说广播是系统里的“全局大喇叭”一提到Android四大组件很多朋友都能说出Activity、Service、ContentProvider的作用唯独对BroadcastReceiver除了知道它是“广播接收器”能收到系统的一些通知之外往往说不清这东西到底解决了什么问题。其实用一个特别接地气的比喻就懂了它就是一台全局大喇叭。你想一下小区门口的对讲机喊一声“快递到了一楼”全楼都听见了谁需要取件谁就下楼各取所需。Android的广播机制就是干这个的。系统、应用都可以向这个大喇叭发消息凡是注册过对应动作action的组件都会收到这段喊话。你不需要知道喊话的人是谁也不要求对方认识你只要大家约定了同一个频道消息就能送达。这套机制最核心的价值是解耦。举一个典型场景你的App里用户登录成功需要在页面上更新几个模块的数据。传统做法是让登录页面持有各个模块的引用或者写一套回调接口层层传递维护成本很高。用广播的话登录成功后发一个自定义Action几个模块各自注册接收器收到消息后自己刷新自己代码边界非常清爽。“全局大喇叭”这个叫法虽然听起来随意但它精准地概括了广播的三个关键特征第一发送者与接收者完全解耦双方不需要互相持有引用第二消息可以跨进程传递不止应用内部使用系统与所有应用之间也靠它通信第三广播是一种“异步的、一对多”的消息分发一次喊话多方响应。对初学者来说广播机制是了解Android系统级通信的最佳切入点因为你只需要理解接收、注册、发送三个动作就能跑通一个完整的收发链路。但想要真正写得稳、写得准光会这三个动作远远不够后面等着你的坑还不少Android 8.0开始收紧隐式广播静态注册静态注册的Receiver存在被系统频繁拉起导致掉电的风险IntentFilter写太宽会莫名其妙收到一堆不该收到的消息。这些都是实战中一定会撞上的问题。这篇文章把广播机制从原理到实操、从标准用法到边界陷阱完整地过一遍大部分内容是我自己在项目里验证过的结论配以必要的细节说明希望帮你少走几条弯路。2. 拆解广播链路从发送、注册到分发的完整旅程要真正搞懂广播机制不能只停留在“sendBroadcast就能发registerReceiver就能收”的层面。你得知道这条链路在系统里是怎么走的理解每个环节发生的时机这样遇到疑难问题时才知道往哪里查。广播的完整流程可以分为三个环节发送、注册、分发。理解它们之间的关系比背下一堆API要重要得多。2.1 发送环节广播消息如何进入系统应用的发送入口是Context.sendBroadcast(Intent)或sendOrderedBroadcast(Intent, receiverPermission)。请注意这里的Intent不是Activity跳转用的那种Intent它只是一个“消息信封”里面主要靠Action、package、type来标定接收范围。你调用sendBroadcast时真正执行的是ContextImpl.sendBroadcast它会通过Binder把Intent传给系统进程中的AMSActivityManagerService。AMS是广播机制的总调度中心所有广播都必须经过它由它统一做分发。这里有一个很多人会忽略的细节广播的发送其实是一个异步过程sendBroadcast返回时广播并没有被接收者处理完它只是被系统接收了真正的分发还在AMS的消息队列中排队。系统每秒能处理的广播数量是有上限的发送太频繁会拖慢AMS进而影响整个系统的响应速度。如果是发送给特定应用的广播可以显式指定包名和类名这种叫显式广播系统会直接找到目标不经过IntentFilter匹配如果不指定则按IntentFilter匹配要经过相对较慢的匹配过程。显式广播效率高、定位准是开发中推荐的用法。2.2 注册环节接收者的两条登记通道接收者要收到广播必须先“登记”。两条通道动态注册和静态注册。动态注册发生在应用进程存活期间。你调用Context.registerReceiver(BroadcastReceiver, IntentFilter)时这个Receiver会连同IntentFilter一起注册到AMS中系统把它记入一个专门维护ReceiverRecord的注册表。只要应用进程活着广播来了就能触发进程死了动态注册的Receiver自动失效不需要也不应该手动去清理那些已经随进程销毁的记录。静态注册则是另一套逻辑。应用打包时清单文件里的receiver标签会被PackageManagerService解析并持久化保存。应用即使完全不运行AMS发送广播时发现匹配的静态Receiver也会先“拉起”这个应用创建一个进程初始化Application实例化对应的Receiver然后调用onReceive。这正是广播机制强大的地方也是危险的地方——它可以把你已退出的应用从后台唤醒生产环境里很多App莫名其妙“被运行”多半就来自这里。2.3 分发环节系统如何找到目标ReceiverAMS收到广播后会根据Intent的Action、权限等信息与注册表比对把所有匹配的Receiver整理成一个分发队列按照优先级顺序逐个派发。发送普通广播一个Receiver收到消息后该广播随即继续不保证顺序发送有序广播则不同系统会按照Receiver声明的priority依次排队你可以中途截断广播使后排Receiver收不到也可以修改广播携带的数据再递给下一个。这里也顺带解释一下“粘性广播sticky”。粘性广播发送后系统会保留这条广播的状态之后注册的Receiver可以立即取到最新的一次结果。典型例子是ACTION_BATTERY_CHANGED你动态注册这个Action时手上马上就能拿到当前电量而不用等下一次电量变化。这种“发一次后到者也能拿到”的特性在特定场景下很好用但粘性广播已被官方标记为不推荐做新项目时优先考虑其他方案。3. 动态注册与静态注册规则变化、决策方法和进程唤醒风险很多初级开发者到现在还在清单文件里写一堆receiver标签觉得“启动快的应用就应该这么干”。这是非常容易踩坑的写法因为2018年之后情况已经完全不同了。3.1 Android 8.0的隐式广播限制从Android 8.0API 26开始官方对静态注册隐式广播做了严格限制绝大多数隐式广播不再允许通过Manifest声明接收。打个比方过去大喇叭一喊不管你在不在家都会被从被窝里叫起来现在系统规定大部分消息只有手机亮屏运行的时候才会递给你你真的完全退出了别指望系统为你开机。为什么要做这个限制本质上是对电量和内存资源的保护。静态注册会带来一个严重后果很多应用为了接收各种高频广播持续把进程“复活”系统每隔一分钟自动发出的TIME_TICK广播能把几十个后台进程全拉起来。开发者舒服了用户手机却痛苦不堪——卡顿、掉电、后台流量体验直线下滑。系统限制隐式广播静态注册等于从设计层面斩断了这种滥用的根基。那么哪些广播还支持静态注册系统默认保留了一批豁免名单比如ACTION_BOOT_COMPLETED、ACTION_LOCKED_BOOT_COMPLETED、ACTION_PACKAGE_ADDED/REMOVED/REPLACED以及用户在系统设置里主动触发的一些“显式化”广播等。不过这份名单随系统版本会调整建议以官方文档的豁免清单为准不要根据历史经验自认为某个Action可以静态注册。我印象里有个实习生写的模块把ACTION_PACKAGE_ADDED做了静态注册结果6.0的设备上能收到、8.0上完全没动静查了半天才发现是这条Action被移出了豁免名单。3.2 何时该用哪种方式一张决策表开发时不要凭感觉选注册方式牢记两条铁律即可与用户当前交互状态强相关的用动态注册需要在应用完全未启动时响应系统关键事件的用静态注册而且尽可能只保留系统白名单内的Action。对比维度动态注册静态注册生命周期随组件注册与注销随应用安装进程不存在时无法收到广播会被系统拉起适用Action任意Action都可动态注册仅系统豁免的部分隐式广播对电量影响局限在应用存活时段存在被动唤醒风险推荐场景前台界面刷新、临时监听开机自启、安装卸载包监听动态注册选在什么时候做最合适也有一个通用准则在onStart/onResume注册在onStop/onPause反注册。如果生命周期没处理好很容易出现广播循环触发、Receiver泄漏这类问题。尤其要注意注册过的Receiver必须在组件退出前尽早unregister不然应用退到后台时系统还在不断发广播进来你的Receiver会在失效的上下文里继续工作。3.3 静态注册的进程拉起与ANR风险静态注册还有一个很隐蔽的性能问题系统拉起应用进程去处理Receiver时这个进程的启动过程是从零开始的Application、所有启动依赖的初始化一步都不能省。onReceive有严格的执行超时限制静态注册和动态注册都一样是10秒左右一旦超出就会被判定ANR弹窗提示“不响应”体验直接崩坏。这就引出广播Receiver的一条铁律绝对不能把耗时任务放在onReceive里也绝对不要在onReceive里启动线程做异步任务。很多人以为“我开了子线程不阻塞主线程就安全了”大错特错。onReceive执行结束系统就认为Receiver已经处理完消息会回收对象。你开的线程可能还在但它引用的BroadcastReceiver对象可能已经被标记为失效后续调用带来的都是不可预期行为。正确姿势是onReceive中只做轻量的收尾和标记具体耗时工作转给Service或WorkManager来执行。之前排查一个App问题时发现对方在BOOT_COMPLETED广播的onReceive里做了大量联网初始化过程耗时近20秒经常触发ANR这种代码看起来“能力很强”实际已经踩了系统的雷。4. 跑通一次自定义广播从发送到接收的实战讲了一堆理论还是要落地。下面用一个完整的例子演示自定义广播的闭环这是你日后写业务逻辑最常用的套路。假设业务里有这样一个需求用户个人信息修改成功后所有展示该数据的页面都要同步刷新。传统方案要维护一份页面引用列表做完修改后通知每个页面刷新非常啰嗦。用广播助清爽很多。4.1 第一步定义Action常量Action本质上就是一个字符串格式建议写成“包名.动作名”减少冲突概率。比如public class UserDataReceiver extends BroadcastReceiver { public static final String ACTION_USER_PROFILE_CHANGED com.example.demo.action.USER_PROFILE_CHANGED; // ... }你也可以直接在一个专门的公共类里集中定义所有Action避免各写各的拼错。4.2 第二步注册动态Receiver在自己的页面里动态注册private BroadcastReceiver profileChangedReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { if (intent.getAction() ! null intent.getAction().equals(ACTION_USER_PROFILE_CHANGED)) { // 刷新页面数据 refreshUserProfile(); } } }; Override protected void onStart() { super.onStart(); IntentFilter filter new IntentFilter(ACTION_USER_PROFILE_CHANGED); registerReceiver(profileChangedReceiver, filter); } Override protected void onStop() { super.onStop(); unregisterReceiver(profileChangedReceiver); }这段代码有三个细节值得写给你看必须把Receiver声明为成员变量不能注册一个匿名内部类后立即释放引用否则反注册时拿不到同一个实例会抛IllegalArgumentException。onStop反注册时机要严谨如果注册了没反注册组件销毁时的日志里会明确提示用户抓狂。注册与反注册必须成对出现。IntentFilter要精确不要用通配Action否则系统会把无关的广播全部投递过来造成性能浪费。4.3 第三步发送广播在用户信息修改完成后调用Intent intent new Intent(ACTION_USER_PROFILE_CHANGED); intent.putExtra(userId, userId); sendBroadcast(intent);建议调用前把一个关键标识通过putExtra放入Intent接收端可以根据“业务标记数据”做双重校验避免收到消息后还要主动去查询数据源。到这里一条完整的广播链路已经跑通了。应用内部从A页面发出广播B、C、D页面同时收到并各自刷新数据页面之间完全解耦。这在实际项目中非常有用能砍掉一大片回调样板代码。5. 有序广播与本地广播两个高频使用场景详解这一节不会把广播的边角料全部扫完而是聚焦业务里真正高频的两个场景有序广播的“拦路截断”价值本地广播的相对安全与性能优势。5.1 有序广播的优先级与截断逻辑普通广播的Receiver收到消息的先后顺序不保证如果业务上对“处理顺序”有硬性要求就得用有序广播。有序广播发出后AMS会拿Intent按照Action匹配到的Receiver按优先级由高到低排队逐个调用onReceive。设置priority的方式是在IntentFilter注册时或清单文件里配置数字越大越先收到。需要注意两点默认优先级是0高优先级可以调用abortBroadcast()截断广播后面的Receiver就收不到了也可以调用setResultData/getResultData传递自己的处理结果继续往后传下一棒的Receiver能读到上一棒留下的数据。这是Android里少有的“中间人接力”设计使用场景不少。举个例子App内发一个登录成功广播一个前置模块可以做统计上报上报完setResultData打上标记后续模块判断一下如果统计失败了就不执行下面的刷新。有了有序广播这种多模块协作流程就能安排得明明白白。不过别滥用有序广播。有序广播的分发路径因为带优先级排队、参数校验等一系列操作整体性能明显低于普通广播。在不需要“严格顺序”的场合尽量用普通广播把复杂度控制下来。5.2 本地广播同进程内通信的稳和快广播跨进程能力很强但很多时候你只是想让App内部的几个模块通信并不希望其他App也监听这条消息。跨进程暴露IntentFilter容易被第三方应用通过构造同样Action的广播“偷听”甚至“伪造”这是隐私风险。应对办法是本地广播官方推荐过的同进程广播方案它的底层实现不再是AMSBinder而是基于Handler的Looper消息分发天然具备“进程内传播”的属性。也就是说本地广播发出的消息只会在App进程内部流转其他应用既不会收到也发不出来。本地广播的关键API是LocalBroadcastManager的sendBroadcast与registerReceiver。相比全局广播它性能更高、更安全、不会拉起其他进程非常适合应用内部模块解耦。但要注意LocalBroadcastManager从Android 12开始被标记为废弃官方建议跨进程需求继续使用全局广播App内通信优先考虑Flow、LiveData、RxBus等组件。不过老项目里LocalBroadcastManager依然大量存在读懂它的机制对接手维护很有帮助。从我的实践来看近几年的新项目里App内通信几乎已经被LiveData和DataBinding取代广播更多时候配合系统事件和跨应用场景使用。6. 广播机制的性能陷阱与安全加固广播机制是把双刃剑用好了是解耦利器用不好就是性能黑洞和安全后门。这一节集中说一说实战中容易踩的坑与加固方案。6.1 广播风暴避免高频隐式广播拖垮主线程所谓广播风暴指短时间内发送大量广播AMS的压力骤增系统的Looper被堆满主线程不断处理广播回调界面卡顿、掉电加速接踵而至。造成广播风暴的常见源头有三种第一网络状态变化、电池变化等系统广播高频发生时你在onReceive里又立刻发送另一个广播形成连锁反应第二应用内自定义高频广播例如每500毫秒向全App同步一次位置第三Receiver内部做了相对重的初始化工作多个Receiver排队叠加。我的建议是给广播加“节流阀”要么用可变时间窗口聚合同类消息例如1秒内多条只发一条要么把业务级高频事件收敛成单一广播由接收端自己细分处理。另一个实操手法是高频事件使用动态注册的Receiver并保证注册期间内不要重复注册同一Action避免AMS里的Receiver列表无限膨胀。6.2 广播的隐私泄露与伪造攻击任何App都可以注册与你一样的Action从而随意监听你的广播内容甚至主动发送伪造广播来触发你的Receiver逻辑。这在跨进程广播场景下威胁特别明显。避免方法有三个重要原则不要通过广播传敏感数据比如明文账号、Token等真要传也得采用加密通道面向跨进程的自定义广播发送时使用setPackage指定包名或者使用带权限的sendBroadcast第二个参数指定权限字符串接收方注册时也带同样的权限在onReceive里对intent的来源、包名、业务参数做校验不通过的直接return。尤其是第三点经常被开发者忽略。即使你用了显式广播或者带包名的发送接收方也应该判断一下sourcePackage以及传入的参数合法性这是一种良好的纵深防御。6.3 谨慎使用后台广播的唤醒能力一个掉电优化案例我记得有一个线上问题排查很有意思。某业务线反馈后台电量消耗高统查后发现有个进程只要亮度变化或网络状态变化就会重新“复活”。仔细查发现它注册了ACTION_SCREEN_OFF、ACTION_USER_PRESENT等好几个系统广播的静态注册而这些Action在较老版本上是可以静态注册的。也就是说只要用户灭屏一次进程就被重新拉起一次Application整套初始化从头执行掉电自然严重。修复方案很简单把这种和“用户在场”强绑定的广播改成动态注册应用进入前台时才监听进程死亡时不拉活反正用户在亮屏时才需要响应。改完掉电问题立刻缓解。这件事给我们的最大教训是不要因为系统允许就滥用静态注册的权限。广播是一把能被随意唤醒的钥匙每一次唤醒都意味着一次进程冷启动成本远比你从“后台进程列表”里看到的多得多。6.4 广播与组件生命周期的协作习惯最后聊一点经验值开发中处理广播与组件生命周期之间的关系有一些约定俗成的习惯很值得坚持。Receiver的注册与反注册要跟持有者生命周期对齐不要跨过组件的onDestroy还保留注册一个Activity或Service里建议只维护一个Receiver用IntentFilter做动作区分不要注册一堆同Action的重复Receiver在Service里处理广播收尾时注意把耗时工作搬运到后台线程并及时stopSelf避免Service被系统回收后又重新唤醒发布广播前务必想清楚是跨进程还是仅进程内这决定了用全局广播还是本地机制。这些良习不是理论是我在维护多个大型App后沉淀的经验。广播虽然只是一段简单的API调用却最考验开发者对运行时生命周期的整体把握能力。写在最后广播机制看似基础实际是Android四大组件中对系统全局影响最大的一层。每次按下sendBroadcast背后都是AMS的一轮分发调度、进程的唤醒与管理涉及资源、性能、安全多个维度。遇到Action收不到、进程被莫名拉起、后台耗电激增这类疑难杂症回到本文梳理的分发链路和注册策略上去排查往往能一击命中。再说个小经验动手开发前先在测试机上用adb shell dumpsys activity broadcasts查看当前所有Receiver的注册情况排查Action冲突和静态注册这类问题非常高效。这也是我在之前项目里定位“8.0上收不到广播”问题时最重要的调试命令之一。