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)
...
这个日志怎么看?我教你三步走:
- 找主线程("main"):看tid=1的那一行,那就是主线程。它的状态是
Blocked,说明被阻塞了。 - 看阻塞原因:主线程在
MainActivity.java:100行,等待锁0x12345678。而这个锁被thread 2持有。 - 看谁持有锁: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的实战案例,到时候我会拿几个我亲手修过的线上问题来拆解,敬请期待。