15. ANR分析与治理:从原理到实战
ANR,全称Application Not Responding,说白了就是应用无响应。每次看到这个弹窗,我心头都会一紧——用户可能已经准备卸载了。今天咱们就彻底把它聊透。
15.1 ANR产生的三大根源
我排查过上百个ANR问题,总结下来,99%的ANR逃不出下面三类。
15.1.1 主线程耗时操作
这是最常见的ANR原因。Android系统规定,主线程超过5秒没处理完输入事件,或者BroadcastReceiver在10秒内没返回,就会触发ANR。
我在项目中遇到过最典型的场景:某个列表页的图片加载直接在主线程做了解码。用户快速滑动时,主线程被卡住,5秒后ANR弹窗就出来了。
主线程不能做的事清单:
- 网络请求(OkHttp、Volley等都不行)
- 大文件读写(SharedPreferences的apply/commit也要注意)
- Bitmap解码(特别是大图)
- 复杂的数据解析(JSON/XML)
- 数据库批量操作
15.1.2 死锁
死锁这东西,说白了就是两个线程互相等着对方释放锁。A线程拿着锁1等锁2,B线程拿着锁2等锁1,结果谁也别想跑。
我曾经在一个多线程下载模块里踩过这个坑。主线程和后台线程同时操作同一个资源池,锁的获取顺序不一致,导致偶发ANR。这个问题在测试环境很难复现,上线后才暴露出来。
避坑指南:我曾经在代码review时发现,有人在一个锁里又调用了另一个需要锁的方法。这种嵌套锁最容易出问题。我的建议是:尽量用Lock替代synchronized,或者用tryLock设置超时。
15.1.3 Binder阻塞
Binder是Android进程间通信的核心机制。当主线程发起Binder调用,而服务端响应慢或者Binder线程池满了,主线程就会卡住。
举个例子:你在主线程调用了getInstalledPackages(),这个调用会跨进程到PackageManagerService。如果系统正在安装应用,这个调用可能等好几秒才返回。嗯,这就是典型的Binder阻塞ANR。
| ANR类型 | 超时时间 | 典型场景 |
|---|---|---|
| 输入事件 | 5秒 | 主线程做耗时操作 |
| BroadcastReceiver | 10秒 | onReceive中做网络请求 |
| Service | 20秒 | onStartCommand执行太久 |
| ContentProvider | 10秒 | query操作耗时过长 |
15.2 Trace文件分析——ANR现场还原
当ANR发生时,系统会在/data/anr/traces.txt生成一份现场快照。这个文件记录了所有线程的堆栈信息,是分析ANR的第一手资料。
我个人习惯把Trace文件分成三步来看:
- 找主线程:搜索"main"或者"Thread-1",看主线程卡在哪个方法上
- 看锁状态:搜索"waiting to lock"或"locked",判断是否有死锁
- 查Binder调用:搜索"Binder: "开头的行,看是否有长时间未返回的调用
举个例子,下面这段Trace就很有代表性:
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x75c0d4f0
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x7f8c5a4000
at com.example.app.MainActivity.onClick(MainActivity.java:45)
- waiting to lock <0x0c41d5e0> (a java.lang.Object)
at android.view.View.performClick(View.java:7125)
...
看到"waiting to lock"了吗?这说明主线程在等一把锁。接下来搜索这个锁的地址0x0c41d5e0,看看谁拿着它不放。
小技巧:我一般用grep -A 20 "main" /data/anr/traces.txt来快速定位主线程堆栈。如果Trace文件太大,可以拉到本地用Sublime或VS Code分析。
15.3 BlockCanary与消息调度监控
Trace文件是事后分析,但更理想的方式是提前发现。BlockCanary就是干这个的——它监控主线程的消息调度,一旦发现某个消息执行超过阈值,就记录堆栈。
15.3.1 BlockCanary原理
BlockCanary的原理其实不复杂。它利用Looper的setMessageLogging方法,在每个消息执行前后打日志。如果前后时间差超过阈值,就说明主线程卡住了。
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
if (x.startsWith(">>>> Dispatching to")) {
// 消息开始执行,记录时间
startTime = System.currentTimeMillis();
} else if (x.startsWith("<<<< Finished to")) {
// 消息执行完毕,计算耗时
long cost = System.currentTimeMillis() - startTime;
if (cost > BLOCK_THRESHOLD) {
// 超过阈值,记录堆栈
dumpStack();
}
}
}
});
你看,核心代码就这么几行。但实际项目中,我建议把阈值设成500ms而不是默认的1000ms。为什么?因为500ms的卡顿用户已经能感知到了,等1000ms再报警,用户可能已经骂人了。
15.3.2 自定义消息调度监控
BlockCanary很好用,但有时候我们需要更细粒度的监控。比如我想知道某个特定Handler的消息执行情况,或者我想把监控数据上报到后台。
我一般会封装一个轻量级的监控工具:
public class LooperMonitor {
private static final long SLOW_THRESHOLD = 500L;
private static final long ANR_THRESHOLD = 5000L;
public static void start() {
Looper.getMainLooper().setMessageLogging(new Printer() {
private long startTime;
private String startToken;
@Override
public void println(String x) {
if (x.startsWith(">>>> Dispatching to")) {
startTime = System.currentTimeMillis();
startToken = x;
} else if (x.startsWith("<<<< Finished to")) {
long cost = System.currentTimeMillis() - startTime;
if (cost > ANR_THRESHOLD) {
// 严重卡顿,上报堆栈
reportANR(startToken, cost, getStack());
} else if (cost > SLOW_THRESHOLD) {
// 轻度卡顿,记录日志
Log.w("LooperMonitor", "Slow: " + cost + "ms " + startToken);
}
}
}
});
}
}
核心要点:
- BlockCanary适合开发阶段使用,定位问题很直观
- 线上环境建议用轻量级方案,只上报堆栈,不做UI展示
- 阈值设置要合理:500ms轻度告警,5秒严重告警
15.4 知识体系总览
下面这张图把ANR的来龙去脉梳理清楚了。从产生原因到分析工具,再到监控方案,一条线串起来。
15.5 实战建议
说了这么多,最后给几条实在的建议:
- 开发阶段:集成BlockCanary,阈值设500ms。每次卡顿都记录下来,养成习惯
- 测试阶段:用adb命令模拟低端机场景,比如
adb shell am broadcast -a android.intent.action.ANR触发ANR,验证监控是否生效 - 线上阶段:用轻量级方案,只上报堆栈和关键信息。我一般用自定义的LooperMonitor,配合公司的APM平台
- 复盘阶段:每次ANR都要写复盘报告,分析根因,制定改进措施。别修完就完事了
最后说一句:ANR治理不是一蹴而就的事。我见过很多团队,上线前突击查ANR,上线后就不管了。其实最好的方式是把ANR监控做成常态化,每次发版前对比ANR率,只降不升。
公众号:蓝海资料掘金营,微信deep3321