ANR 的类型与检测手段

说到 ANR,你可能会想:不就是应用无响应嘛,点两下没反应就弹个框。但真实情况远没那么简单。我刚开始做性能优化时,常常被 ANR 搞得焦头烂额——明明 log 里只有一句“Application Not Responding”,原因却千奇百怪。后来踩了不少坑,才把 ANR 的类型和检测手段摸透。

说白了,ANR 的本质是 Android 系统在监控关键组件时,发现它超时未响应。系统会弹一个对话框,然后,嗯,你的用户可能就直接卸载了。我们先把 ANR 的种类理清楚,再看怎么用工具抓住它。

一、ANR 的四种核心类型

Android 官方把 ANR 分成五大类,但实际开发中常见的是下面四种。我把它们整理成了一张表,方便你对照。

类型 触发场景 超时阈值 典型原因
输入事件 ANR 主线程 5 秒内未处理完输入事件(按键、触摸) 5 秒 主线程耗时 I/O、复杂布局、死锁
广播 ANR 前台广播 onReceive() 10 秒未返回,后台 60 秒 前台 10s / 后台 60s 在 onReceive 里做了网络请求或数据库操作
服务 ANR 前台服务生命周期超时,后台服务 200 秒 前台 20s / 后台 200s 服务里堆了大任务,没及时调用 stopSelf
Provider ANR ContentProvider 发布后 10 秒未完成 onCreate 10 秒(注意:很少有触发) Provider 初始化时加载大量数据

这里有个坑要提醒你:后台广播的 60 秒超时只在 Android 8.0 之前成立。8.0 之后隐式广播被限制,大部分后台广播不再直接触发 ANR,但系统会默默杀掉进程。我曾经在一个老项目里踩过这个坑——升级到 8.0 后用户反馈一些功能无响应,查了半天才发现是广播注册方式变了。

二、检测手段:从原始日志到自动化埋点

知道了类型,下一步就是怎么抓到它们。我按从简单到复杂的顺序,给你讲讲我最常用的几种手段。

1. 系统日志(logcat)快速定位

最简单的方式:当 ANR 发生时,logcat 里会出现类似下面的关键行:

W/ActivityManager(  123): ANR in com.example.app (com.example.app/.MainActivity)
W/ActivityManager(  123): PID: 4567
W/ActivityManager(  123): Reason: Input dispatching timed out (Waiting to send non-key event...)
W/ActivityManager(  123): Load: 2.3 / 1.5 / 1.0
W/ActivityManager(  123): CPU usage from 12345ms to 0ms ago:
W/ActivityManager(  123):   100% 4567/com.example.app: 95% user + 5% kernel
W/ActivityManager(  123):   0.2% 123/system_server: 0.1% user + 0.1% kernel

看到“ANR in”和“Reason”字段了吗?它直接告诉你是哪个进程、什么原因。我习惯马上看 CPU 使用率——如果进程自己占了 100% 且持续很长时间,基本是主线程死循环;如果总 CPU 不高,那多半是主线程在等锁。

小技巧:用 adb logcat -b events 查看事件日志,可以捕获 ANR 的精确时间戳和包名。配合 grep -E "am_anr|reason" 过滤,效率翻倍。

2. /data/anr/traces.txt 抓取堆栈

logcat 只是引子,真正的大脑在 trace 文件里。发生 ANR 后,系统会把所有线程的堆栈 dump 到 /data/anr/traces.txt。我会第一时间 pull 出来:

adb root
adb pull /data/anr/traces.txt .

打开文件,找到你自己的应用进程(通常在文件开头附近),重点关注主线程的堆栈。比如:

"main" prio=5 tid=1 Blocked
  at com.example.app.MainActivity$1.run(MainActivity.java:30)
  - waiting to lock <0x...> (a java.lang.Object)
  at android.os.Handler.handleCallback(Handler.java:883)

看到“Blocked”和“waiting to lock”了吗?这是典型的锁竞争。我有一次排查一个 ANR,trace 里主线程在等一个锁,而那个锁被另一个线程持有超过 10 秒——结果那个线程在搞网络重连,主线程只能干等。

3. StrictMode 主动探测

如果等到用户反馈 ANR 再修复,那太被动了。我习惯提前用 StrictMode 在开发阶段就抓住主线程的“坏习惯”。在 Application 里开启:

public void onCreate() {
    if (BuildConfig.DEBUG) {
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork()
                .penaltyLog()
                .penaltyDeathDialog() // 模拟 ANR 弹窗,让开发马上知道
                .build());
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                .detectLeakedClosableObjects()
                .penaltyLog()
                .build());
    }
}

注意那个 penaltyDeathDialog——它会在主线程违反策略时直接弹一个类似 ANR 的对话框,强迫你立即修复。不过别在 release 版本开,否则用户会骂你。

4. Choreographer 帧率监控

输入事件 ANR 往往从丢帧开始。你可以监听 Choreographer 的回调,当一帧超过 16ms 时记录堆栈:

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTime = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTime != 0) {
            long diff = (frameTimeNanos - lastFrameTime) / 1000000;
            if (diff > 100) { // 超过 100ms 就算严重卡顿
                // 主动 dump 主线程堆栈
                String stack = Log.getStackTraceString(Looper.getMainLooper().getThread());
                // 上报堆栈到埋点服务
            }
        }
        lastFrameTime = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

这个方法我在一个直播应用里用过:用户反馈视频播放时会突然卡死,用 Choreographer 监控发现直播推流线程抢占 CPU 导致主线程长时间得不到调度。后来做了线程优先级调整,ANR 率降低了 80%。

三、SVG 知识结构图

下面我用一张图把 ANR 类型和检测手段串起来,方便你作为知识卡片保存。

ANR 类型与检测手段 输入事件 ANR 5s 超时 广播 ANR 前台10s / 后台60s 服务 ANR 前台20s / 后台200s Provider ANR 10s 初始化超时 ❶ logcat 日志捕获 ❷ /data/anr/traces.txt ❸ StrictMode ❹ Choreographer 帧监控 ❺ 自定义 ANR Watchdog ❻ APM 平台埋点 根因定位:线程堆栈 + CPU 负载 结合时间戳还原 ANR 前后 10s 行为 建议:组合使用至少 3 种手段,才能覆盖 90% 的 ANR 场景

四、落地实践:从检测到埋点的关键点

检测手段只是第一步,真正要解决问题还得靠 自动化埋点。我总结几个关键原则:

  • 堆栈采样要高频:不要等 ANR 发生才抓,Choreographer 发现掉帧超过 500ms 时就主动 dump 一次主线程堆栈。我在一个 IM 应用里把这个提前量做到了 300ms,ANR 发现率提升 3 倍。
  • 保留 CPU 上下文:dump 堆栈的同时,务必记录最近 5 秒的 CPU 使用率、进程线程状态。否则你看到堆栈里主线程在等锁,却不知道那个锁被谁占用——前功尽弃。
  • 避坑指南:我曾经遇到过一个 case,logcat 里没有“ANR in”,但用户确实卡死了。后来发现是系统因为低内存杀死了系统服务,导致弹不了 ANR 对话框。这种情况下只能靠 /proc/self/oom_adj 和低内存日志来辅助判断。
警告:正式环境不要使用 penaltyDeathDialog!StrictMode 的弹窗会在用户手机屏幕上直接爆出崩溃对话框,非常影响体验。建议只在 debug 包开启,或者在 release 包降级为 penaltyLog 配合内部埋点。

你看,ANR 的类型无非就那几种,但检测手段是组合拳。从 logcat 到 trace 文件,从 StrictMode 到 Choreographer,再到自研的 Watchdog,每个工具都有自己的适用范围。下一篇我们会深入讲如何构建一个生产环境可用的 ANR 埋点框架,把今天说的这些手段都揉进去。嗯,先消化一下这张图吧。