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文件分成三步来看:

  1. 找主线程:搜索"main"或者"Thread-1",看主线程卡在哪个方法上
  2. 看锁状态:搜索"waiting to lock"或"locked",判断是否有死锁
  3. 查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的来龙去脉梳理清楚了。从产生原因到分析工具,再到监控方案,一条线串起来。

ANR分析与治理知识体系 ANR产生原因 主线程耗时操作 死锁(锁竞争) Binder阻塞 分析工具 Trace文件分析 BlockCanary 消息调度监控 治理方案 异步化改造 锁优化 Binder调用优化 核心思路:预防为主,监控为辅,快速定位,彻底根治 开发阶段用BlockCanary,线上用轻量监控,ANR发生后分析Trace文件

15.5 实战建议

说了这么多,最后给几条实在的建议:

  • 开发阶段:集成BlockCanary,阈值设500ms。每次卡顿都记录下来,养成习惯
  • 测试阶段:用adb命令模拟低端机场景,比如adb shell am broadcast -a android.intent.action.ANR触发ANR,验证监控是否生效
  • 线上阶段:用轻量级方案,只上报堆栈和关键信息。我一般用自定义的LooperMonitor,配合公司的APM平台
  • 复盘阶段:每次ANR都要写复盘报告,分析根因,制定改进措施。别修完就完事了

最后说一句:ANR治理不是一蹴而就的事。我见过很多团队,上线前突击查ANR,上线后就不管了。其实最好的方式是把ANR监控做成常态化,每次发版前对比ANR率,只降不升。


公众号:蓝海资料掘金营,微信deep3321