30、BroadcastReceiver调试:广播注册、有序广播、粘性广播、超时问题

广播这玩意儿,在Android系统里就像个传话筒。但传话筒用不好,要么消息丢了,要么卡死主线程。今天咱们就聊聊广播调试的那些坑。

广播注册的两种姿势

广播注册分静态和动态,我习惯叫它们「清单注册」和「代码注册」。区别很明显:

注册方式 生命周期 调试要点
静态注册(AndroidManifest.xml) 应用安装后一直存在 注意Android 8.0后大部分隐式广播被限制
动态注册(registerReceiver) 跟随组件生命周期 记得在onPause/onDestroy中unregister

我在项目中遇到过一个问题:动态注册的广播接收器在Activity销毁后还在接收广播。说白了就是忘了在onDestroy里调用unregisterReceiver。结果呢?内存泄漏,还可能导致空指针崩溃。

注意:动态注册后不注销,如果广播发送频繁,你的应用会一直收到广播,即使界面已经关闭。我曾经排查过一个ANR问题,最后发现就是广播接收器没注销,在后台疯狂处理广播。

有序广播的调试

有序广播(Ordered Broadcast)是按优先级顺序传递的。每个接收者可以截断广播,也可以修改结果数据。

调试有序广播时,我一般会关注三点:

  • 优先级设置:通过intent-filter的android:priority属性设置,数值越大优先级越高
  • 截断广播:调用abortBroadcast()可以阻止后续接收者
  • 结果传递:使用setResultExtras/getResultExtras传递数据

举个例子,你想想看:

// 发送有序广播
Intent intent = new Intent("com.example.MY_ORDERED_BROADCAST");
sendOrderedBroadcast(intent, null);

// 高优先级接收者
public class HighPriorityReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle extras = new Bundle();
        extras.putString("data", "来自高优先级");
        setResultExtras(extras);
        // 如果不想让低优先级收到,可以截断
        // abortBroadcast();
    }
}

调试时有个技巧:在adb shell里用dumpsys activity broadcasts可以查看当前系统中注册的所有广播接收器及其优先级。嗯,这个命令我经常用。

粘性广播的坑

粘性广播(Sticky Broadcast)已经被标记为deprecated了。为什么?因为它会导致状态不一致。

粘性广播的特点是:发送后广播会一直存在,新注册的接收器能立即收到最后一条粘性广播。听起来很方便对吧?但问题来了:

  • 权限控制困难,任何应用都能读取粘性广播数据
  • 调试时很难复现问题,因为广播状态是持久的
  • Android 5.0之后已经不建议使用
我的建议:如果你还在用粘性广播,赶紧换成LocalBroadcastManager或者EventBus。我在一个老项目里见过粘性广播导致的问题——应用重启后,新注册的接收器收到了一条过期的广播,直接触发了错误的业务逻辑。

广播超时问题

广播超时是ANR的常见原因之一。系统给广播接收器的执行时间默认是10秒(前台广播)或60秒(后台广播)。

为什么会超时?说白了就是你在onReceive里做了耗时操作。记住:onReceive运行在主线程

我曾经排查过一个线上ANR问题,日志显示:

Broadcast of Intent { act=com.example.ACTION_SYNC flg=0x10 } 
  took 15000ms, timeout = 10000ms

一看就知道,有人在onReceive里直接做了网络请求。正确的做法是:

public class SafeReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 只做轻量操作
        Intent serviceIntent = new Intent(context, MyService.class);
        context.startService(serviceIntent);
        
        // 或者使用goAsync
        final PendingResult pendingResult = goAsync();
        new Thread(() -> {
            // 耗时操作
            pendingResult.finish();
        }).start();
    }
}
关键点:使用goAsync()可以让广播接收器在后台线程执行,但必须在10秒内调用finish()。否则系统还是会认为超时。

调试工具与技巧

我常用的广播调试三板斧:

  1. adb shell dumpsys activity broadcasts:查看所有注册的广播接收器
  2. adb logcat -s "BroadcastQueue:B":过滤广播相关的日志
  3. 设置-开发者选项-显示所有ANR:开启后能捕获广播超时

另外,在Android Studio的Profiler里也能看到广播的发送和接收情况。嗯,这个功能很多人不知道。

知识体系总览

下面这张图总结了广播调试的核心要点:

BroadcastReceiver调试 注册方式 静态注册 动态注册 广播类型 普通广播 有序广播 常见问题 内存泄漏 ANR超时 调试核心要点 1. 动态注册必须配对unregisterReceiver 2. onReceive中不要做耗时操作(超过10秒会ANR) 3. 有序广播注意优先级和截断逻辑 4. 粘性广播已废弃,尽快迁移

避坑指南

最后分享几个我踩过的坑:

  • 我曾经在onReceive里调用了ContentResolver.query,结果数据库操作太慢导致ANR。后来改成用AsyncQueryHandler或者直接丢给Service处理。
  • 我曾经调试有序广播时发现低优先级接收器收不到广播,查了半天才发现是高优先级调用了abortBroadcast()。嗯,这个坑很多人会踩。
  • 我曾经在Android 8.0设备上发现静态注册的广播不生效,后来才知道大部分隐式广播已经被限制了。解决办法是改用动态注册或者JobScheduler。

广播调试其实不难,关键是要理解它的生命周期和超时机制。记住一句话:onReceive不是做苦力的地方

专注资料整理