ANR 基础概念:从“卡死”到“崩溃”的真相
你好,我是这次课程的主讲。今天我们来聊聊 Android 开发中一个让人又爱又恨的东西——ANR。
你可能遇到过这种情况:用户点了个按钮,屏幕突然卡住,紧接着弹出个“应用无响应”的对话框。用户心里想的是“这破 App 真垃圾”,而你作为开发者,心里想的是“完了,又要背锅了”。
ANR 其实就是那个“背锅侠”的学名。说直白点,它就是一个系统监控机制:当应用的主线程长时间不响应,系统就会判定它“已死”,然后弹框让用户选择是继续等还是直接关掉。别小看这个机制,在我做过的项目里,ANR 往往是线上 Crash 率飙升的罪魁祸首。
什么是 ANR?—— 系统在等你,但你让系统失望了
Android 系统对每一个应用都有一个“耐心值”。主线程如果 5 秒内没响应输入事件,或者广播接收器 10 秒内没执行完,系统就会认为这个应用“已经凉了”。说白了,就是你的主线程干了不该它干的事。
我记得刚入行时,自己写了个 app,一启动就弹 ANR。查了半天,原来是 onCreate() 里做了个网络请求。代码很简单:
// 错误示范:主线程做网络请求
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 我当年就在这犯的错
URL url = new URL("http://example.com/api/data");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 这行代码会阻塞主线程,直接引发 ANR
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
}
你想想看,网络请求有时候一次就得跑好几秒,主线程被卡住,用户点啥都没反应,系统不弹 ANR 才怪呢。
ANR 的触发条件 —— 系统什么时候会“发火”?
其实系统挺“善良”的,它不会因为你偶尔卡个 200 毫秒就弹框。但要是一超时,它就不客气了。下面这张表总结了我这些年遇到的触发场景:
| 场景 | 超时时间 | 典型原因 |
|---|---|---|
| 输入事件(按键/触摸) | 5 秒 | 主线程做耗时操作 |
| 广播接收器(前台) | 10 秒 | onReceive 里做了网络/IO |
| 广播接收器(后台) | 60 秒 | 后台广播处理不及时 |
| Service(前台启动) | 20 秒 | onCreate/onStartCommand 耗时 |
| ContentProvider | 10 秒 | query/update 执行数据库大操作 |
为什么要记住这些数字?因为调试的时候,系统日志里会明确告诉你“超时类型”是哪种。如果你不知道对应的时间阈值,可能连问题方向都找不对。
ANR 的四大类型 —— 每种都是一种“死法”
系统不会无缘无故弹 ANR,它会把具体原因分类。我习惯把这四种类型称为“四大酷刑”,每一种背后的死法都不太一样。
1. 输入调度 ANR(Input Dispatch ANR)
这是最常见的一种。用户点了屏幕或者按了物理按键,系统把事件通过 InputDispatcher 发给你。如果你的主线程正在忙别的事(比如卡在 HandlerThread 里),事件 5 秒内没被消费掉,ANR 就来了。
我之前在做一个即时通讯应用时,聊天界面经常出现“无响应”。后来一查,原来是列表刷新时在主线程做了图片压缩。5 秒的窗口说没就没,压一张大图就超时了。
adb shell dumpsys activity processes 能看到主线程卡在哪个方法里。
2. 广播 ANR(Broadcast ANR)
广播接收器的超时阈值和输入不一样:前台广播 10 秒,后台广播 60 秒。但别以为 60 秒很长就好欺负。我之前调试过一个很经典的案例:有个第三方推送的广播接收器,在 onReceive() 里做了数据库写操作,结果恰好赶上大表迁移,一次写入跑了 15 秒。前台广播直接炸了。
广播 ANR 还有一个特别坑的地方:它不会让你立刻看到日志。有时候用户反馈“点了没反应”,你一看日志才发现是几分钟前的广播超时。因为系统只有在超时那一刻才会打 log,后面再弹框。
// 错误示范:广播里做耗时操作
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 我见过有人在广播里做数据库备份
// 一次备份可能跑 30 秒,直接触发 ANR
backupDatabase(context); // 不要在 onReceive 里干这个!
}
}
3. 服务 ANR(Service ANR)
Service 的 ANR 相对少见,但一旦出现往往是大问题。前台启动的 Service 有 20 秒超时窗口。我遇到过一个案例,某个定位服务在 onStartCommand() 里初始化了 GPS 模块,结果因为 GPS 信号差一直卡在注册回调上。20 秒一到,系统直接判它死刑。
这里有个经验:Service 的生命周期回调里,不能跑耗时任务。实在要跑,用 IntentService 或者手动起个工作线程。
JobScheduler 或 WorkManager。否则 Service ANR 只是第一步,下一步可能直接被系统杀掉进程。
4. ContentProvider ANR
ContentProvider 的 ANR 是最容易被忽视的。很多人觉得 ContentProvider 就是个数据访问层,不会有性能问题。实际上,它的超时阈值只有 10 秒。我之前查过一个案例:某个第三方 SDK 的 ContentProvider 在 query() 方法里做了复杂的联表查询,数据量大时跑了 8 秒。表面上没超时,但如果主线程同时做别的操作,两件事加起来就把 10 秒窗口吃掉了。
有一次我排查一个“间歇性 ANR”的问题,抓了 20 多次堆栈,最后发现每次主线程都卡在 ContentResolver.query() 上。说白了,问题不在你的代码里,而是那个 ContentProvider 的实现在主线程上跑得太慢。
一张图看懂 ANR 整体结构
讲了这么多,我把四大类型和它们的触发关系画成了一张图。你可以对照着理解:
这张图里,最上面的入口是 Android 系统的 ANR 监控机制,它像个哨兵一样盯着你的应用。一旦主线程犯了“超时”的错,它就会根据具体场景归到四类里,最终弹出那个令所有开发者头疼的对话框。
为什么 ANR 这么难查?
原因其实很简单:它不是每次都复现的。同样的代码,用户 A 可能遇到 ANR,用户 B 就没事。为什么?因为 ANR 的触发依赖于主线程当时的负载状态。说白了,只在“卡到一定时间点”的时候才弹,而不是每次慢都弹。
这给排查带来了很大难度。你没法靠“多跑两次”来复现。必须学会看 log、抓堆栈、分析 trace 文件。后面几章我会带着你一步步把这些技能练起来。
onResume() 里。平时测试机跑得飞快,一到用户的中低端机上,解码时间直接飙到 6 秒。结果 ANR 率暴涨 300%。后来我在所有主线程回调里加了 StrictMode 检测,才把这类问题扼杀在预发布阶段。
别急,后面我们会逐一攻克每种类型的 ANR。今天先把概念夯实了,后面实战才不会迷路。