Android重学笔记|别再滥用广播了

774 阅读7分钟

零、前言

在Android开发中,广播(Broadcast)曾被视为组件通信的“万能钥匙”——跨界面更新、进程间数据传输、系统事件监听,似乎一切场景都能用一行sendBroadcast()轻松解决。然而,随着应用复杂度提升和Android系统的迭代,这种“简单粗暴”的通信方式正逐渐暴露出性能损耗、安全隐患与架构腐化的致命问题:高频广播引发的ANR(应用无响应)、裸奔的隐式Intent被恶意拦截、静态注册导致的内存泄漏……

本篇博客将以系统机制剖析为起点,直指广播滥用的典型场景(如全局事件传递、敏感数据跨进程暴露),解析其背后的性能瓶颈与设计代价。

一、广播的核心机制:重新理解系统级通信的成本

1. 广播的底层调度流程
  • 发送阶段:调用 sendBroadcast() 后,Intent 被序列化并通过 Binder 跨进程传递到 ActivityManagerService(AMS)。AMS 根据 IntentFilter 匹配接收器,按优先级排序后,将广播插入到 BroadcastQueue 队列中等待分发。
  • 分发阶段:AMS 通过 ApplicationThread 与目标进程通信,反序列化 Intent 并触发接收器的 onReceive()。若接收器未在主线程注册,系统会创建临时 Handler 投递到主线程(动态注册可指定线程)。
  • 性能瓶颈序列化开销:跨进程通信需将 Intent 数据序列化为 Parcelable,大对象或高频发送时显著影响性能。 队列竞争:系统级 BroadcastQueue 处理所有应用的广播,高并发场景下易引发延迟或 ANR。
2. 系统广播的“特权”与代价
  • 特权场景:系统广播(如 ACTION_BOOT_COMPLETED)由系统进程发送,允许静态注册,但需声明权限和满足应用激活条件。
  • 隐式成本:应用监听高频系统广播(如网络状态变化)时,即使处于后台也会被 AMS 唤醒,增加电量消耗和内存占用。

二、广播的误用场景:你的代码是否在“犯罪”?

1. 跨进程滥用:全局广播的裸奔风险
  • 案例:使用隐式广播传递用户敏感数据(如 Token),未添加权限控制,导致恶意应用窃取信息。

  • 漏洞代码

    • // 危险:隐式广播传递敏感数据
      Intent intent = new Intent("com.example.USER_UPDATE");
      intent.putExtra("token", "eyJhbGciOiJIUzI1NiIsInR...");
      sendBroadcast(intent);
      
2. 生命周期失控:动态注册的泄漏陷阱
  • 案例:在 Activity 中动态注册接收器,但未在 onDestroy() 中注销,导致 Activity 实例无法回收。

  • 内存泄漏堆栈

    • └─ Activity (泄漏)
          └─ BroadcastReceiver
              └─ ActivityManagerService (持有引用)
      
3. 粘性广播的“僵尸”残留
  • 案例:使用已废弃的 sendStickyBroadcast() 发送状态更新,导致 Intent 长期驻留系统内存,引发后续接收器逻辑混乱。

三、正确使用广播的六大法则

1. 权限双保险:发送与接收的权限校验
  • 发送方:通过 sendBroadcast(intent, permission) 声明私有权限。

  • 接收方:在 <receiver> 标签或动态注册时添加 android:permission 校验。

  • 自定义权限(示例):

    • <permission
          android:name="com.example.PRIVATE_BROADCAST"
          android:protectionLevel="signature" />
      
2. 显式广播优先:精准打击代替广撒网
  • 显式指定包名

    • Intent intent = new Intent("com.example.ACTION_LOGIN");
      intent.setPackage("com.target.app"); // 限定接收方
      sendBroadcast(intent);
      
3. 动态注册的黄金搭档:Lifecycle 组件
  • 使用 **LifecycleObserver**自动注销

    • class MyObserver implements LifecycleObserver {
          private BroadcastReceiver receiver;
      
          @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
          void register(Context context) {
              receiver = new MyReceiver();
              context.registerReceiver(receiver, filter);
          }
      
          @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
          void unregister(Context context) {
              context.unregisterReceiver(receiver);
          }
      }
      
4. 后台发送的合规方案
  • Android 9+ 适配:启动前台服务后再发送广播:

    • // 启动前台服务
      startForegroundService(new Intent(this, MyService.class));
      // 发送广播
      sendBroadcast(new Intent("com.example.BACKGROUND_ACTION"));
      
5. 有序广播的节制使用
  • 避免滥用优先级:高优先级接收器应快速处理并传递结果,防止阻塞后续逻辑。

  • 结果传递规范

    • public void onReceive(Context context, Intent intent) {
          Bundle results = getResultExtras(true);
          results.putString("key", "processed_data");
          setResultExtras(results);
      }
      
6. 系统广播的“白名单”策略
  • 仅监听必要事件:如 BOOT_COMPLETEDTIMEZONE_CHANGED,避免监听 CONNECTIVITY_CHANGE 等高频广播。
  • 静态注册豁免检查:参考 官方豁免列表

四、替代方案

1. 应用内通信: LiveData + ViewModel
  • 场景:Activity/Fragment 间数据同步。

  • 优势:生命周期感知、无内存泄漏、数据驱动 UI。

  • 代码示例

    • // ViewModel 中定义 LiveData
      class MyViewModel extends ViewModel {
          private MutableLiveData<String> data = new MutableLiveData<>();
          LiveData<String> getData() { return data; }
          void updateData(String value) { data.postValue(value); }
      }
      
      // Activity 中观察
      viewModel.getData().observe(this, value -> {
          textView.setText(value);
      });
      
2. 后台任务调度: WorkManager
  • 场景:替代广播实现的定时任务或条件任务。

  • 示例:设备充电时执行数据同步

    • Constraints constraints = new Constraints.Builder()
          .setRequiresCharging(true)
          .build();
      
      OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SyncWorker.class)
          .setConstraints(constraints)
          .build();
      
      WorkManager.getInstance(context).enqueue(request);
      

五、总结:广播的“生存指南”

在编写 sendBroadcast() 前,先问自己:这个数据真的需要惊动整个系统吗?

  • 保留广播的场景:系统事件监听(如开机完成)、跨应用合规通信(需权限控制)、需要有序处理的场景(如拦截短信)。
  • 立即重构的场景:高频应用内事件(如按钮点击)、敏感数据传输、后台保活逻辑。

六、高频面试问题

Q1: LocalBroadcastManager的原理是什么?为什么它比全局广播更高效?

1.工作原理

(1)本地化注册与发送

  • 不依赖系统服务:与全局广播通过系统级 ActivityManagerService 处理不同,LocalBroadcastManager 完全在应用进程内管理广播的注册和分发。
  • 维护本地接收器列表:内部通过一个 HashMap 维护所有注册的接收器及其 IntentFilter,发送广播时直接遍历匹配的接收器。

(2)基于 Handler 的消息分发

  • 主线程消息队列:使用 Handler 将广播分发到主线程的消息队列,确保接收器的 onReceive() 在主线程执行,避免多线程问题。
  • 同步或异步处理:广播发送后立即触发接收器处理,无需等待系统调度。

(3)无跨进程通信

  • 广播的 Intent 对象在内存中直接传递,无需通过 Binder 机制进行序列化与反序列化。

2.对比全局广播

LocalBroadcastManager的高效性源于其设计上的优化,避免了系统广播机制的复杂流程和IPC开销,使得广播的发送和接收更加快速且资源消耗更低。同时,由于仅限于应用内部,安全性自然提高。

对比维度LocalBroadcastManager全局广播
通信范围仅限于同一应用内可跨应用(需权限)
IPC 开销无(进程内通信)有(依赖 Binder 跨进程通信)
系统服务依赖不依赖 ActivityManagerService依赖系统服务分发广播
序列化开销直接传递对象,无需序列化Intent 需序列化/反序列化
权限检查无需复杂权限验证需检查发送/接收方权限
系统广播队列竞争无,直接分发受系统广播队列调度影响,可能延迟
后台限制不受 Android 高版本后台限制影响Android 9+ 后台应用无法发送隐式广播
安全性数据不泄露到其他应用需防范恶意应用拦截或窃听
Q2: 什么是粘性广播(Sticky Broadcast)?为什么Android 5.0后不再推荐使用?

1.粘性广播(Sticky Broadcast)

粘性广播是一种特殊类型的广播,其核心特性是发送的 Intent 会持续驻留在系统的消息容器中,即使广播发送完成后,新注册的接收器仍能接收到该广播。这种机制适用于需要持久化关键状态信息的场景,例如电源状态变化、网络状态更新等,确保后续注册的接收器能立即获取最新状态

  1. 持久性:广播发送后,Intent 会一直保留在系统中,直到被显式移除(如调用 removeStickyBroadcast()
  2. 无需实时接收:接收器无需在发送时已注册,后续注册的接收器仍可获取广播数据
  3. 跨组件通信:支持跨应用广播(需权限控制),但可能存在安全隐患

2.Android 5.0 后弃用粘性广播的原因

自 Android 5.0(API 21)起,Google 将粘性广播标记为 deprecated,主要出于以下考虑:

(1)安全性问题

粘性广播的 Intent 长期驻留系统,可能被恶意应用窃取敏感数据(如权限状态、设备信息)。即使使用权限控制,若权限管理不当,仍可能导致隐私泄露。

(2)资源占用与性能问题

长期驻留的 Intent 会占用系统内存,尤其在频繁发送粘性广播时,可能引发内存泄漏或性能下降。