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 发生才抓,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 埋点框架,把今天说的这些手段都揉进去。嗯,先消化一下这张图吧。