ANR 基础概念:从“卡死”到“崩溃”的真相

你好,我是这次课程的主讲。今天我们来聊聊 Android 开发中一个让人又爱又恨的东西——ANR。

你可能遇到过这种情况:用户点了个按钮,屏幕突然卡住,紧接着弹出个“应用无响应”的对话框。用户心里想的是“这破 App 真垃圾”,而你作为开发者,心里想的是“完了,又要背锅了”。

ANR 其实就是那个“背锅侠”的学名。说直白点,它就是一个系统监控机制:当应用的主线程长时间不响应,系统就会判定它“已死”,然后弹框让用户选择是继续等还是直接关掉。别小看这个机制,在我做过的项目里,ANR 往往是线上 Crash 率飙升的罪魁祸首。

ANR 全称:Application Not Responding,应用无响应。

什么是 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 秒的窗口说没就没,压一张大图就超时了。

提示:遇到输入 ANR,第一步不是看代码逻辑,而是看主线程堆栈。用 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 或者手动起个工作线程。

注意:从 Android 8.0 开始,后台 Service 启动限制更严格了。如果你还想在后台做长耗时操作,建议改用 JobScheduler 或 WorkManager。否则 Service ANR 只是第一步,下一步可能直接被系统杀掉进程。

4. ContentProvider ANR

ContentProvider 的 ANR 是最容易被忽视的。很多人觉得 ContentProvider 就是个数据访问层,不会有性能问题。实际上,它的超时阈值只有 10 秒。我之前查过一个案例:某个第三方 SDK 的 ContentProvider 在 query() 方法里做了复杂的联表查询,数据量大时跑了 8 秒。表面上没超时,但如果主线程同时做别的操作,两件事加起来就把 10 秒窗口吃掉了。

有一次我排查一个“间歇性 ANR”的问题,抓了 20 多次堆栈,最后发现每次主线程都卡在 ContentResolver.query() 上。说白了,问题不在你的代码里,而是那个 ContentProvider 的实现在主线程上跑得太慢。

一张图看懂 ANR 整体结构

讲了这么多,我把四大类型和它们的触发关系画成了一张图。你可以对照着理解:

ANR 架构与触发机制总览 Android ANR 监控机制 输入调度 ANR 广播 ANR 服务 ANR ContentProvider ANR 主线程阻塞 5 秒 按键/触摸事件未消费 onReceive 超时 前台 10s / 后台 60s Service 生命周期超时 前台启动 20s 窗口 Provider 操作超时 query/update 超 10s 系统弹出“应用无响应” 最终结果:用户可以选择“等待”或“关闭应用” 严重影响用户体验和 App 评分

这张图里,最上面的入口是 Android 系统的 ANR 监控机制,它像个哨兵一样盯着你的应用。一旦主线程犯了“超时”的错,它就会根据具体场景归到四类里,最终弹出那个令所有开发者头疼的对话框。

为什么 ANR 这么难查?

原因其实很简单:它不是每次都复现的。同样的代码,用户 A 可能遇到 ANR,用户 B 就没事。为什么?因为 ANR 的触发依赖于主线程当时的负载状态。说白了,只在“卡到一定时间点”的时候才弹,而不是每次慢都弹。

这给排查带来了很大难度。你没法靠“多跑两次”来复现。必须学会看 log、抓堆栈、分析 trace 文件。后面几章我会带着你一步步把这些技能练起来。

避坑指南:我曾经在一个线上版本中犯过一个低级错误——把一个大图片的解码放到了 onResume() 里。平时测试机跑得飞快,一到用户的中低端机上,解码时间直接飙到 6 秒。结果 ANR 率暴涨 300%。后来我在所有主线程回调里加了 StrictMode 检测,才把这类问题扼杀在预发布阶段。

别急,后面我们会逐一攻克每种类型的 ANR。今天先把概念夯实了,后面实战才不会迷路。