第131篇BroadcastReceiver:静态注册与动态注册的存亡

简介: 本文深入剖析 BroadcastReceiver 的三层认知:API 层(能收广播)、机制层(知限制规则)、工程层(懂替代方案)。详解静态/动态注册、`goAsync()` 与 ANR 避坑、隐式广播限制(Android 8+)、有序广播淘汰原因及 `WorkManager` 等现代替代方案,助你真正掌握广播本质。

先把结论放在前面:BroadcastReceiver 的答案分三层——"能收到广播"是 API 层、"知道哪些广播被限制"是机制层、"知道替代方案是什么"是工程层。 Android 8 之后隐式广播被大幅限制、9 之后后台广播进一步收紧,能答到第三层的人才算真懂。

BroadcastReceiver 看起来是个简单组件,追问起来全是版本变化和限制规则。这篇按"是什么 → 怎么工作 → 限制在哪 → 怎么办"四步讲清。

两种注册,两条投递路径

静态注册写在清单里,由系统在应用启动时(ContentProvider 初始化之后、Application.onCreate 之前)建立接收器,进程被杀时也能收到——但受后台广播限制。

<receiver android:name=".NetworkChangeReceiver" android:exported="false">
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>

动态注册通过 registerReceiver 在运行时建立,必须与注册所在的 Context 生命周期对应——Activity 上注册要 unregisterReceiver,否则泄漏。

class MainActivity : AppCompatActivity() {
   

    private val receiver = object : BroadcastReceiver() {
   
        override fun onReceive(context: Context?, intent: Intent?) {
   
            // 注意:onReceive 跑在主线程
            handleNetworkChange(intent)
        }
    }

    override fun onStart() {
   
        super.onStart()
        // API 33+ 必须显式传导出标志
        registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION),
            Context.RECEIVER_NOT_EXPORTED)
    }

    override fun onStop() {
   
        super.onStop()
        unregisterReceiver(receiver)
    }
}

跨 API 的注册写法要注意:ContextCompat.registerReceiver(...) 已经在新 androidx 里统一处理了导出标志的兼容问题,比直接调 registerReceiver 更稳。

最常见的坑是

onReceive 里做耗时操作,超过前台广播时限触发 ANR。

onReceive 在主线程执行,系统对前台广播给了约 10 秒窗口,超时会 ANR;后台广播窗口更短(约 30 秒,且从 Android 8 起很多已不再投递)。有人在这里直接发起网络请求、写数据库、解析大 JSON,结果是 UI 卡顿甚至无响应,而日志里看不出是哪次广播触发的。

规范写法是把耗时部分转出去:

override fun onReceive(context: Context, intent: Intent) {
   
    // 只做轻量判断 + 转交,onReceive 尽快返回
    val pending = goAsync()
    scope.launch {
   
        try {
   
            syncData(intent.getStringExtra(EXTRA_ID))
        } finally {
   
            pending.finish()   // 必须调,否则进程可能被判定异常
        }
    }
}

goAsync() 返回 PendingResult,处理完必须 finish()。但要注意它的约束:进程仍可能在 finish() 之前被杀,所以它是"提高成功率"而不是"保证成功"。真正需要保证完成的工作应该交给 WorkManager 或前台服务。

还有一个更隐蔽的坑:用 BroadcastReceiver 的经验法则是前提显式声明,兜底显式实现,上线前少出事。落到具体动作是三条。① 能用显式广播就不发隐式——setPackage 或 setComponent 显式指定目标,避免被别的应用截获;② 敏感操作用权限保护——自定义 action 要配 <permission> 并在发送方声明,否则任何应用都能伪造;③ 注册要收敛——不要用动态注册长期监听高频广播(ACTION_SCREEN_ON、BATTERY_CHANGED),这类改用 ContentObserver 或 SensorManager 更合适。

还有三个话题值得单独说,因为它们是"用过"与"用明白"的分界线。

话题一:有序广播与结果回传。 orderedBroadcast 让接收者按优先级依次处理,每个接收者可以用 setResultCode 与 setResultData 传回结果,最终调用方通过 sendOrderedBroadcast(..., resultReceiver, ...) 拿到。但这条路在现代开发里已经不推荐:有序广播会阻塞、无法取消、优先级在后台限制下形同虚设。需要"请求-响应"式交互时,正确的做法是 Messenger、绑定(AIDL)或单进程内的直接调用。

话题二:本地广播。 LocalBroadcastManager 曾用于应用内广播(不进系统、不被其它应用收到),但它已在 AndroidX 里标记废弃。原因是它解决的是"不想要外部收到"这个需求,而这个需求用显式广播(setPackage/setComponent)+ 权限就能更直接地满足,不需要额外一套框架。

话题三:广播与状态管理的分工。 很多项目把"数据刷新"也做成广播,结果形成"广播套广播"的链路,排查时根本看不出谁触发了谁。更清晰的做法是:状态变化走数据层(Flow/回调),广播只表达"系统层面的事件"。判断标准很简单——如果这个事件的所有处理方都在同一进程内、且能通过某个共享对象拿到数据,那它就不该是广播。

还有一个实践细节值得单说:onReceive 与 onReceiveWithResult 的区别。 后者能返回 setResultCode / setResultData,并且可以主动调 goAsync() 延长处理窗口;前者只是单向通知。同理,LocalBroadcastManager 版与 ContextCompat 版在行为上也已不同。新代码统一用 ContextCompat.registerReceiver + ContextCompat.sendBroadcast,避免 API 级别的行为差异。

三条高频追问

问:Android 8 之后哪些隐式广播还能收?

结论是几乎不能收,除非在清单里显式声明该 action。被限制的是"隐式广播"——即只声明 action 不指定接收组件的那类。系统给出的可注册白名单主要是这几类:启动完成(注意 8.0 起不再包含)、锁屏相关(部分)、输入法相关、ACTION_MY_PACKAGE_REPLACED、以及"以 ACTION_ 前缀结尾的系统广播"这个兜底规则。实际开发里能依赖的极少,所以主流做法是改用 JobScheduler/WorkManager 的约束条件替代广播。

问:广播的优先级 priority 有什么用?

它只影响同一进程内接收器的处理顺序(数值越大越先),跨进程比较的是 Intent 的 priority 属性,且只对有序广播(orderedBroadcast)有意义。它不能用来"抢先"别人的广播,也不能保证及时收到——所以不要用高优先级广播做"紧急通知"这类需求。

问:广播能携带大对象吗?

不能。广播走 Binder 事务,同样有约 1MB 限制。传大对象的正确做法是传 ID,让接收方自己查——这条判据与 Activity 传值、ContentProvider 返回值是同一个(都受 Binder 事务限制),说出来能体现对底层机制的一致理解。

问:怎么用广播做跨进程通信?

现代方案是 Messenger(基于 Handler 的消息封装)或 AIDL,但如果是"通知某个状态变了",优先考虑 ContentProvider 的 ContentObserver 或直接用 回调 + Binder。广播只适合"一对多的状态通知",不适合"一对一的请求-响应"。

落到项目里怎么做

一条能直接落地的规则:通知类需求优先用 Flow/LiveData + 进程内共享,跨进程才用广播,跨场景且需要保证完成用 WorkManager。 广播的定位应该收缩到"系统事件通知"这一类。

配套实践三条:① 自定义 action 统一前缀(com.demo.action.X)并集中成常量,配上自定义权限;② 所有动态注册统一走 ContextCompat.registerReceiver 并在配对的 onStop 注销;③ 用 Lint 拦截过时的隐式广播(系统已废弃的那些),在编译期而不是运行期暴露。

给正在准备面试的你

这题的层次差在"知不知道限制"。推荐这条答题线:先讲静态/动态两种注册与投递路径 → 再讲 onReceive 在主线程、goAsync 的用法与约束 → 然后主动点出 Android 8 隐式广播限制、9 之后后台收紧(主动讲限制是最高分的动作)→ 落到替代方案(JobScheduler/WorkManager 约束、显式广播、进程内 Flow)→ 最后补 Binder 1MB 限制与大对象传 ID 的判据。能把"限制"和"替代方案"连起来讲,答案就有层次了。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:WorkManager:后台任务的最终归宿

下一篇预告:ContentProvider:跨进程数据共享的标准入口

有任何问题欢迎在评论区留言交流。

相关文章
|
2天前
|
缓存 编译器 API
第119篇 扩展属性与扩展伴生:工具方法的进阶形态
本节详解Kotlin扩展属性与伴生对象扩展:扩展属性无backing field,本质是静态getter/setter方法,每次访问均重计算;扩展伴生对象则为第三方类“添加静态方法”,语法简洁、语义清晰。二者均属编译期静态分派,不可覆盖,是检验Kotlin底层理解的关键点。
21 2
|
2天前
|
调度 Android开发 容器
第126篇Fragment 事务与回退栈:add、replace、show、hide
Fragment事务异步队列执行,非调用即生效;回退栈存操作记录而非实例。`show/hide` 快但常驻内存、不可回退;`replace`+栈可回退但重建视图。选型关键:是否需回退?是否敏感内存?——是判断题,非知识点。
21 1
|
2天前
|
安全 Java 定位技术
第117篇 Kotlin 阶段面试通关地图:高频题怎么连成网
这是Kotlin面试的结构化复习地图,非知识点罗列,而是将60+篇内容按「六大知识块×三层深度」(会用/懂机制/能取舍)分层归因,直击面试区分度核心。助你快速定位保底分与拉分点,掌握高频题答题骨架、交叉跳转逻辑及三大避坑指南,真正把知识织成可检索、可推演、可落地的活网络。
26 1
|
2天前
|
安全 Java 编译器
第022篇 try-catch-finally 与 try-with-resources:资源释放正确姿势
本文用“场景—决策—踩坑—效果”四步法,讲透try-catch-finally与try-with-resources的工程实践。重点解析finally中return吞异常、手写关闭漏资源、twr如何保留主异常并挂suppressed等高频面试坑点,附可运行代码对比,助你面试答出深度与记忆点。
21 0
|
2天前
|
安全 Java 编译器
第097篇 Kotlin 与 Java 互操作:JvmStatic、JvmOverloads 与平台类型
本文详解 Kotlin 与 Java 混编的四大底层规则:空安全在 Java 侧失效、默认参数需 `@JvmOverloads` 才生成重载、属性/对象成员编译为 `getXxx()` 或 `INSTANCE`、顶层声明落入文件类。涵盖 `@JvmStatic`、`@JvmField`、`@JvmSynthetic`、`fun interface` 等关键注解的原理与避坑实践,助你打通真实工程与面试高频考点。
14 0
|
2天前
|
JSON 安全 编译器
第105篇 Kotlin 编译期陷阱:Companion、默认参数与混淆
本文深入剖析 Kotlin 五大编译期陷阱:data class 混淆失配、空安全跨语言失效、泛型擦除导致类型检查失灵、伴生对象初始化时机异常、内联类与序列化/反射冲突。聚焦“为什么 release 崩而 debug 正常”,直击机制层本质,助你面试从容拆解“Kotlin 坑”的底层原理。
21 0
|
2天前
|
缓存 安全 Java
第109篇 Mutex 与信号量:协程世界的锁
本节详解协程环境下共享资源保护:`synchronized` 因跨挂起、线程与协程语义错配而失效;推荐使用 `Mutex`(单协程互斥)和 `Semaphore`(N并发限流),二者均支持挂起不阻塞线程。结合 Android 实际场景,厘清锁适用边界——单值用 `StateFlow.update`,计数用原子类,复合操作才用 `Mutex`。附典型坑点与面试高频四连问。
17 0
|
2天前
|
Java 编译器 API
第080篇 Lambda 语法精讲:it、尾随 lambda 与隐式接收者
Kotlin Lambda面试核心在四个易错点:尾随vs括号传参(仅限最后一个参数)、单参隐式`it`与嵌套遮蔽、`return`非局部返回(仅inline函数允许)、块式Lambda返回类型由末行推断。具名参数+inline标注+显式label是避坑关键。
16 0
|
2天前
|
安全 Java 编译器
第061篇 Kotlin 与 Java 的关系:同一 JVM 上的两门语言
Kotlin与Java互操作≠对称兼容!核心差异在混编边界:平台类型致空安全失效、`internal`编译为public、默认参数需`@JvmOverloads`、data class字段private final使Gson反序列化失配。真功夫在收尾——注解规范、ProGuard保留元数据、协程作用域管控。
27 0
|
2天前
|
存储 设计模式 安全
第056篇 新时间 API:LocalDateTime 取代 Date 的理由
移动端时间处理易出线上事故:`SimpleDateFormat` 静态共享致线程不安全;跨时区“昨天”判断偏差;字符串截取本地化日期失效。Java 8 `java.time` 核心在于厘清**三类时间语义**:`Instant`(UTC瞬时点)、`LocalDateTime`(无时区墙上时间)、`ZonedDateTime`(含时区规则,支持夏令时)。存储用 `Instant`,展示才绑定时区;格式化必传 `Locale`;`YYYY`≠`yyyy`;`Period`(日历量)与 `Duration`(物理量)不可混用。
34 0

热门文章

最新文章