2、ANR基础:什么是ANR、ANR产生的原因、ANR日志初步解读

各位好,欢迎来到第二章。上一章我们聊了Native Crash,这一章咱们把焦点转到另一个让Android开发者头疼的问题——ANR。

ANR,全称Application Not Responding,应用无响应。说白了,就是你的App卡住了,系统看不下去了,弹个对话框问用户「是要继续等,还是直接关掉?」。我刚开始做Android开发时,第一次见到这个弹窗,心里一紧——完了,又要被测试小姐姐追着跑了。

2.1 什么是ANR?

ANR是Android系统的一种保护机制。当应用的UI线程(也就是主线程)在规定时间内没有完成某个操作,系统就会判定该应用无响应,进而弹出ANR对话框。

具体来说,系统会监控以下几种场景的超时:

场景 超时时间 触发条件
输入事件分发 5秒 主线程未处理完触摸/按键事件
广播接收 10秒 BroadcastReceiver的onReceive()未返回
服务执行 20秒 Service的onCreate/onStart等未完成
ContentProvider 10秒 ContentProvider的query/insert等操作超时

嗯,这里要注意:不同Android版本的超时时间可能略有差异,但大体上就是这个范围。我个人习惯把5秒作为红线——任何可能超过5秒的操作,都不应该放在主线程。

2.2 ANR产生的原因

ANR的根因,说白了就一句话:主线程被阻塞了。但阻塞的原因五花八门,我挑几个最常见的来说。

2.2.1 主线程做了耗时操作

这是最常见的原因。比如在主线程里直接做网络请求、读写大文件、解析JSON、加载大图等。你想想看,主线程本来要处理用户点击、刷新界面,结果它跑去下载数据了,用户点啥都没反应,系统不弹ANR才怪。

典型错误代码:

// 千万别这么写!
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    // 在主线程做网络请求
    String result = HttpUtils.get("https://api.example.com/data");
    // 或者解析大文件
    JSONArray array = new JSONArray(result);
    // 或者写数据库
    db.insertData(array);
}

我的建议:所有耗时操作都扔到子线程去。用Handler、AsyncTask、RxJava或者协程都行,就是别在主线程干重活。

2.2.2 死锁

死锁这个问题,我在项目中遇到过好几次。两个线程互相等待对方释放锁,结果谁也动不了。如果主线程恰好参与了这场「死局」,那ANR就来了。

举个例子:

// 线程A持有锁1,等待锁2
synchronized (lock1) {
    Thread.sleep(100);
    synchronized (lock2) {
        // 做点啥
    }
}

// 线程B持有锁2,等待锁1
synchronized (lock2) {
    Thread.sleep(100);
    synchronized (lock1) {
        // 做点啥
    }
}

如果主线程就是线程A,那它永远等不到锁2,因为线程B拿着锁2不放。结果就是主线程卡死,ANR弹出。

避坑指南:我曾经在一个项目里,因为两个模块的锁顺序不一致,导致线上ANR率飙升。排查了两天才找到根因。后来我定了个规矩:所有锁必须按固定顺序获取,比如先获取小ID的锁,再获取大ID的锁。这样能从根本上避免死锁。

2.2.3 Binder调用阻塞

主线程调用Binder服务时,如果服务端响应慢,主线程也会被阻塞。比如调用系统服务获取联系人列表,或者跨进程调用其他App的服务。这些调用如果超时,主线程就卡住了。

2.2.4 主线程被无限循环占用

这个比较少见,但一旦出现就很坑。比如某个View的onDraw方法里写了死循环,或者某个动画回调里做了大量计算。主线程被死死占住,根本没法处理用户事件。

2.3 ANR日志初步解读

当ANR发生时,系统会在/data/anr/traces.txt里记录一份日志。这个文件是分析ANR的「第一手资料」。我每次遇到ANR问题,第一件事就是去拉这个文件。

来看一个典型的ANR日志片段:

----- pid 12345 at 2024-01-15 10:30:00 -----
Cmd line: com.example.myapp

"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x75123456
  | sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x7f123456
  | state=Blocked stack=0x7ff123456
  | heldMutexes=
  at com.example.myapp.MainActivity.doSomething(MainActivity.java:100)
  - waiting to lock <0x12345678> (a java.lang.Object) held by thread 2
  at com.example.myapp.MainActivity.onCreate(MainActivity.java:50)
  at android.app.Activity.performCreate(Activity.java:7000)
  ...

"Thread-2" prio=5 tid=2 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x75123457
  | sysTid=12346 nice=0 cgrp=default sched=0/0 handle=0x7f123457
  | state=Blocked stack=0x7ff123457
  | heldMutexes=
  at com.example.myapp.BackgroundWorker.run(BackgroundWorker.java:30)
  - waiting to lock <0x12345679> (a java.lang.Object) held by thread 1
  at java.lang.Thread.run(Thread.java:900)
  ...

这个日志怎么看?我教你三步走:

  1. 找主线程("main"):看tid=1的那一行,那就是主线程。它的状态是Blocked,说明被阻塞了。
  2. 看阻塞原因:主线程在MainActivity.java:100行,等待锁0x12345678。而这个锁被thread 2持有。
  3. 看谁持有锁:Thread-2也在等待锁0x12345679,而这个锁被主线程持有。好了,死锁无疑。

关键信息提取:

  • pid:进程ID,方便你定位是哪个App
  • Cmd line:包名,确认是哪个应用
  • 线程状态:Blocked、Runnable、Waiting等。Blocked最常见
  • 堆栈信息:代码执行到哪一行,一目了然
  • 锁信息:waiting to lock / held by,死锁的「铁证」

除了traces.txt,logcat里也会有ANR相关的日志。搜索ANR in关键字,就能找到类似这样的信息:

I/ActivityManager: ANR in com.example.myapp (com.example.myapp/.MainActivity)
    Reason: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.)
    Load: 2.5 / 1.8 / 1.2
    CPU usage from 0ms to 5000ms:
    com.example.myapp: 45% = 30% user + 15% kernel
    system_server: 12% = 8% user + 4% kernel
    ...

这里能看到ANR的原因(Input dispatching timed out)、CPU负载、以及各个进程的CPU占用率。如果App的CPU占用率很高,说明主线程可能在做大量计算;如果很低,那多半是死锁或者Binder调用卡住了。

一个小技巧:我习惯把traces.txt和logcat的ANR日志一起看。traces.txt告诉你「卡在哪里」,logcat告诉你「为什么卡」。两者结合,定位问题快很多。

好了,这一章的内容就到这里。ANR的基础知识,说白了就是:主线程别干重活,锁别乱用,日志要会看。下一章我们会深入分析ANR的实战案例,到时候我会拿几个我亲手修过的线上问题来拆解,敬请期待。