4、ANR问题分析:ANR产生原理、traces.txt解读、主线程阻塞分析、常见ANR场景
ANR,全称Application Not Responding,中文叫“应用无响应”。这大概是Android开发中最让人头疼的问题之一。用户正刷着短视频,突然弹个框“XX应用无响应”,要么等要么关——体验直接归零。
我这些年排查过的ANR,少说也有上百个了。有些是代码写得不小心,有些是系统层面的坑。今天我就把ANR的底裤扒干净,从原理到实战,一条龙讲清楚。
核心观点:ANR的本质是主线程在规定时间内没有完成特定操作。说白了,就是主线程被“卡住”了,系统等得不耐烦了。
4.1 ANR的产生原理
系统是怎么判断ANR的?它有一个“超时机制”。不同类型的操作,超时时间不一样:
| ANR类型 | 超时时间 | 触发场景 |
|---|---|---|
| 输入事件无响应 | 5秒 | 按键、触摸等事件没被处理 |
| 广播超时 | 前台10秒 / 后台60秒 | BroadcastReceiver的onReceive()执行太久 |
| 服务超时 | 前台20秒 / 后台200秒 | Service的onCreate()、onStartCommand()等耗时过长 |
| ContentProvider超时 | 10秒 | ContentProvider的query()、insert()等操作卡住 |
系统内部有一个Watchdog线程,它像个监工一样盯着主线程。一旦发现主线程在规定时间内没响应,就会触发ANR流程:
- 系统收集当前所有线程的堆栈信息
- 写入
/data/anr/traces.txt文件 - 弹出ANR对话框给用户
- 如果应用有
ANRHandler回调,会触发它
嗯,这里要注意:ANR不是崩溃。崩溃是进程挂了,ANR是进程还在,但主线程“僵死”了。我见过不少新手把ANR当崩溃处理,方向就错了。
4.2 traces.txt解读——ANR的“案发现场”
traces.txt是ANR分析的第一手资料。每次ANR发生,系统都会把当时的线程堆栈dump到这个文件里。我个人习惯,拿到traces.txt后先看三样东西:
- 主线程状态:是RUNNABLE还是BLOCKED?
- 主线程堆栈:卡在哪个方法里?
- 锁信息:有没有死锁?
一个典型的traces.txt片段长这样:
----- 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=0x...
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x...
at com.example.myapp.MainActivity.onClick(MainActivity.java:50)
- waiting to lock <0x0c41f2a0> (a java.lang.Object)
at android.view.View.performClick(View.java:7441)
at android.view.View.performClickInternal(View.java:7418)
...
看到tid=1就是主线程。状态是Blocked,说明它在等一把锁。锁的地址是0x0c41f2a0。接下来要找谁持有了这把锁。
继续往下翻,找到持有锁的线程:
"AsyncTask #1" prio=5 tid=15 Runnable
| group="main" sCount=0 dsCount=0 flags=0 obj=0x...
| sysTid=12360 nice=0 cgrp=default sched=0/0 handle=0x...
at com.example.myapp.DataLoader.doInBackground(DataLoader.java:30)
- locked <0x0c41f2a0> (a java.lang.Object)
at com.example.myapp.DataLoader.doInBackground(DataLoader.java:15)
...
真相大白了:AsyncTask #1持有了锁,而且它正在做耗时操作(doInBackground)。主线程想拿同一把锁,只能干等。这就是典型的锁竞争导致的ANR。
小技巧:traces.txt里如果看到主线程状态是Runnable但堆栈停在某个方法里很久,那多半是CPU被抢占了,或者方法本身太耗时。我曾经遇到过一个案例,主线程在BitmapFactory.decodeStream()里卡了8秒——因为加载了一张4K的大图。
4.3 主线程阻塞分析——谁在拖后腿?
主线程阻塞的原因,我总结下来就四大类:
| 阻塞类型 | 典型表现 | 常见原因 |
|---|---|---|
| IO操作 | 主线程读写文件、网络请求 | SharedPreferences写入、数据库查询、网络请求 |
| 锁竞争 | 主线程等待其他线程释放锁 | 同步块、Lock、死锁 |
| 计算密集型 | 主线程做大量计算 | 图片解码、JSON解析、正则匹配 |
| Binder调用 | 主线程等待Binder返回 | 跨进程调用、系统服务响应慢 |
怎么定位?我一般用“三步法”:
- 看堆栈:traces.txt里主线程停在哪个方法?
- 看时间:这个方法执行了多久?有没有循环?
- 看上下文:是用户操作触发的,还是后台任务触发的?
举个例子。有一次线上ANR频发,traces.txt显示主线程卡在View.onDraw()里。我一看,自定义View的onDraw()里居然做了Path的复杂计算,每次绘制都要重新算一遍。嗯,这明显是过度绘制的问题。后来改成缓存Path对象,ANR就消失了。
避坑指南:我曾经遇到过一种“假ANR”——traces.txt里主线程状态是Runnable,堆栈停在Looper.loop()里。这种情况其实不是主线程卡住,而是系统误判了。原因可能是CPU负载过高,导致主线程虽然没阻塞,但调度延迟超过了5秒。这时候要去看系统日志,确认是不是CPU飙到了100%。
4.4 常见ANR场景与解决方案
根据我的经验,90%的ANR都集中在下面几个场景里:
场景一:主线程做网络请求
这是最经典的ANR。很多新手直接在onClick()里写HttpURLConnection.connect()。网络一慢,5秒就超时了。
解决方案:用协程、RxJava或AsyncTask把网络请求扔到子线程。Android 4.0以后,主线程做网络请求会直接抛NetworkOnMainThreadException,但有些老项目还在用旧API,要小心。
场景二:SharedPreferences写入
SharedPreferences的apply()是异步的,但commit()是同步的。如果在主线程调commit(),而且数据量很大,就会卡住主线程。
解决方案:一律用apply()代替commit()。如果数据量大,考虑用MMKV或DataStore。
场景三:数据库操作
Room或SQLite的查询如果在主线程执行,数据量一大就ANR。我记得有个项目,用户列表查询返回了5000条数据,主线程直接卡了3秒。
解决方案:Room默认不允许在主线程查询,但可以加allowMainThreadQueries()——千万别加。用LiveData或Flow配合协程,异步查询。
场景四:广播接收器耗时
BroadcastReceiver的onReceive()默认在主线程执行。如果在里面做耗时操作,比如写文件、网络请求,前台广播10秒就ANR。
解决方案:在onReceive()里启动一个IntentService或JobIntentService,把耗时操作移到子线程。
场景五:死锁
两个线程互相等待对方释放锁,就会死锁。主线程一旦参与死锁,必ANR。
解决方案:用synchronized时注意锁的顺序。可以用tryLock()加超时,避免无限等待。traces.txt里如果看到多个线程都在waiting to lock,那基本就是死锁了。
这张图是我自己总结的ANR分析流程。从ANR发生到最终修复,每一步都有对应的工具和方法。你想想看,如果每次ANR都按这个流程走一遍,90%的问题都能在半小时内定位到根因。
最后说一句:ANR分析没有银弹。traces.txt是起点,但不是终点。有时候还需要结合logcat、systrace、甚至内存快照才能找到真因。但只要你掌握了今天讲的这些基础,大部分ANR都难不倒你。