4. 广播 ANR:BroadcastReceiver onReceive 执行超时

BroadcastReceiver 是 Android 中最常见的 ANR 触发源之一。系统在调度广播消息时,会为每个同步广播的 onReceive 方法设置一个执行超时计时器。如果 onReceive 在限定时间内没有返回,系统就会判定该广播发生 ANR。前台广播超时阈值为 10 秒,后台广播为 60 秒(不同厂商可能微调)。

关键区别:只有同步广播(sendBroadcast)会触发 ANR;有序广播(sendOrderedBroadcast)同样会阻塞,但异步广播(sendBroadcast + 标记异步线程)不会。广播 ANR 的日志关键词是 Broadcast of Intent + timeout + BR never finished。

4.1 现象识别

症状用户感知系统表现
弹框“xxx 无响应”卡住当前应用,可选择“等待”或“关闭”ANR 日志中记录 BroadcastReceiver 超时
广播接收后界面卡死触摸无反应,Activity 不刷新主线程被 onReceive 阻塞,无法处理输入事件
系统广播(如开机广播、网络变化)导致全机响应慢多个应用同时 ANR,甚至 SystemUI 崩溃AMS 输出多条广播超时记录
快速判断规则:若日志中出现 Broadcast timeout 且 receiver 包名属于你的应用,则 100% 是广播 ANR。若只出现 Input dispatching timed out 但无广播日志,需交叉验证。

4.2 数据采集

当广播 ANR 发生时,系统会在 /data/anr/traces.txt 中记录主线程堆栈。同时 logcat 中会输出详细调度信息。以下是关键数据采集方法:

  1. 抓取 ANR 日志:在测试机上提前开启全量 logcat 缓存,触发 ANR 后执行 adb bugreport。
  2. 定位广播超时日志:在 bugreport 或 logcat 中搜索 Broadcast of Intent { act=xxx } 以及 timeout of broadcast。
  3. 获取主线程堆栈:在 traces.txt 中找到对应进程的堆栈,重点关注主线程是否停在 BroadcastReceiver.onReceive 内部。
  4. 检查接收器注册方式:区分是静态注册(AndroidManifest.xml)还是动态注册(registerReceiver),二者超时策略一致,但触发条件不同。
# 触发 ANR 后立即抓取日志
adb shell dumpsys dropbox --print data_app_anr

# 或直接查看 traces.txt
adb pull /data/anr/traces.txt
grep -A 20 "main" traces.txt  # 主线程堆栈

# 在 logcat 中过滤广播 ANR
adb logcat -b main -b system -v time | grep -E "Broadcast.*timeout|ANR.*Receiver"

4.3 原因定位

广播 ANR 的根本原因只有一个:主线程在 onReceive 中执行了耗时的同步操作。以下是完整的故障树分析:

常见误判:onReceive 本身默认运行在主线程。即使你在 onReceive 末尾启动了新线程,但如果 onReceive 自己阻塞了(比如 wait()、join()、网络请求),主线程仍然不返回。

广播 ANR 故障树

广播 ANR 故障树 广播超时(ANR) onReceive 执行超时 系统调度延迟 注册与生命周期 主线程 I/O 网络请求 持久化数据库 同步锁等待 主线程被其他任务阻塞 系统资源紧张 注册后未及时取消 系统广播频率过高 查看 traces.txt 主线程堆栈 定位 onReceive 内阻塞行 (如 FileInputStream, SQLite) 检查主线程 Looper 消息队列 是否存在前序耗时消息 (如 Layout, Draw) 检查 AndroidManifest 中 broadcast 注册策略 + 动态注册时机

日志分析范例

在 bugreport 中找到以下日志块即可确认广播 ANR:

07-25 14:30:01.123  1234  5678 W ActivityManager: Broadcast of Intent { act=com.example.ACTION_SLOW flg=0x10 }
07-25 14:30:01.123  1234  5678 W ActivityManager: Receiver currently running: ComponentInfo{com.example.app/com.example.app.MyReceiver}
07-25 14:30:01.123  1234  5678 W ActivityManager: Timeout of broadcast BroadcastRecord{...} - receiver=ReceiverList{...} started 10000ms ago
07-25 14:30:01.124  1234  5678 W ActivityManager: Killing process 6789 (com.example.app) for ANR in broadcast
07-25 14:30:01.124  1234  5678 I DropBoxManagerService: add data_app_anr

traces.txt 中的主线程堆栈样例:

"main" prio=5 tid=1 Blocked
  at java.net.SocketInputStream.socketRead0(Native Method)
  at java.net.SocketInputStream.read(SocketInputStream.java:150)
  at java.net.SocketInputStream.read(SocketInputStream.java:121)
  at okhttp3.internal.connection.RealCall.call(RealCall.java:135)
  at com.example.app.MyReceiver.onReceive(MyReceiver.java:25)
  at android.app.ActivityThread.handleReceiver(ActivityThread.java:3694)
定位技巧:堆栈显示 onReceive 内调用了 OkHttp 网络请求(第25行)。由于主线程不允许网络操作(Android 3.0+),这必然导致阻塞超时。即使使用 strictmode 也会抛出网络禁止异常,但若 catch 住则直接阻塞。

4.4 修复动作

修复广播 ANR 的核心原则:永远不要在 onReceive 中执行任何耗时操作。一旦 onReceive 返回,系统就认为广播已完成。推荐以下修复方案:

方案实现方式适用场景
1. 启动 IntentService在 onReceive 中 startService(new Intent(...)),让后台线程处理需要一次性处理的后台任务
2. 使用 goAsync() + 子线程在 onReceive 中调用 goAsync(),获得 PendingResult,然后在子线程中执行耗时操作,最后 finish()需要利用广播接收器的生命周期(如接收结果)
3. 改为 JobScheduler / WorkManager在 onReceive 中调度一个 Job,让系统在合适时机执行周期性或可延迟的任务
4. 使用 LocalBroadcastManager(已废弃,建议 LiveData)避免静态广播,改用应用内事件分发仅限应用内的广播通信

推荐代码示例(goAsync 方案):

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 关键:先调用 goAsync,告诉系统我在后台处理
        final PendingResult pendingResult = goAsync();
        
        // 使用 AsyncTask 或 Executor 在线程中执行耗时操作
        new Thread(() -> {
            try {
                // 耗时工作:网络请求、数据库操作等
                doHeavyWork(context, intent);
            } finally {
                // 必须调用 finish 通知系统
                pendingResult.finish();
            }
        }).start();
    }
    
    private void doHeavyWork(Context ctx, Intent i) {
        // 示例:保存文件
        // ...
    }
}
goAsync 注意事项:
- goAsync 必须在 onReceive 返回前调用。
- PendingResult 的超时时间与接收器相同(前台 10s/后台 60s),超时后也会 ANR。
- 每个 BroadcastReceiver 只允许调用一次 goAsync,多次调用会导致 crash。
- 不要将 PendingResult 传递给其他线程导致生命周期混乱。

4.5 复测

修复后执行以下验证步骤:

  1. 单元测试:使用 robolectric 或 AndroidJUnitRunner 模拟广播触发,执行时间应 < 1ms(仅调度返回)。
  2. 压力测试:连续发送 50 次广播,观察主线程是否阻塞。可使用 adb shell am broadcast 命令批量测试:
# 模拟发送自定义广播 50 次(测试机/模拟器)
for i in {1..50}; do
  adb shell am broadcast -a com.example.ACTION_TEST_FAST --user 0
done

# 监控主线程状态
adb shell "while true; do ps -T -p $(pid com.example.app) | grep main; sleep 0.5; done"
  1. 全日志检查:抓取完整的 logcat -b all,确认无 Broadcast timeout 日志出现。
  2. 回归测试:覆盖所有注册的广播接收器,确保每一个 onReceive 都无耗时操作。

4.6 预防

将以下规则纳入团队开发规范:

  • 代码审查强制规则:所有 onReceive 方法体不得超过 5 行代码,且不得有 I/O、网络、数据库、同步锁、Thread.sleep 等。
  • 使用 lint 检查:在 build.gradle 添加自定义 lint 规则,禁止在 BroadcastReceiver 中调用阻塞 API。
  • 动态注册的生命周期管理:Activity/Fragment 在 onPause/onStop 中 unregisterReceiver,避免残留接收器持续收到广播。
  • 首选 WorkManager:对于后台任务,推荐 WorkManager 替代广播。
  • 前台广播减少:系统敏感广播(如 CONNECTIVITY_CHANGE, BATTERY_LOW)采用 android:exported="false" 或限定版本。
自动化预防方案:在 CI 流水线中加入 ANR 检测脚本,每次提交后自动运行广播压力测试,若出现 data_app_anr dropbox 条目则构建失败。

经过以上六个阶段,广播 ANR 可得到系统性解决与长期防范。