1. ANR基础知识:什么是ANR、产生原因、系统检测机制、常见场景

1.1 什么是ANR?说白了就是「App无响应」

ANR,全称 Application Not Responding。翻译成大白话就是:你的应用卡住了,系统看不下去了,弹个对话框告诉用户「这个应用无响应,你要不要关掉它?」。

我和很多工程师交流过,大家第一反应都是「ANR不就是卡顿吗?」其实不完全对。卡顿是掉帧,ANR是系统判定主线程在规定时间内没有完成某些关键操作。系统会直接亮红牌。

嗯,这里要注意:ANR不是崩溃,但比崩溃更让用户反感。崩溃至少是瞬间闪退,ANR却要用户看着那个灰色弹窗,等几秒甚至几十秒,最后只能选择「等待」或「关闭」。我曾在用户反馈里看到过「App点不动,想砸手机」——这就是ANR的威力。

核心定义:

  • 主线程(UI线程)被阻塞超过5秒(输入事件)
  • 广播接收器(BroadcastReceiver)在10秒内未执行完毕
  • 服务(Service)在20秒内未执行完关键生命周期方法

1.2 为什么会触发ANR?我复盘过几个真实案例

触发ANR的根本原因只有一个—主线程干了不该它干的活。但我做了这么多年性能优化,发现大家往往低估了隐藏的坑。

  • 耗时I/O操作:直接在主线程读写文件、操作数据库。我曾经遇到一个直播间,每次进房间都要从SharedPreferences读取配置,读取了10MB的数据——ANR妥妥的。
  • 网络请求:虽然现在都用OkHttp+RxJava了,但新同学有时候会忘了切换线程。我在代码评审时见过直接在主线程调用Retrofit同步请求的……
  • 复杂计算:比如解析JSON、压缩图片、加密解密。加密操作尤其危险,尤其是在低端机上。
  • 锁竞争:主线程等一个锁,而锁被其他线程持有。这是最难排查的一类。
  • Binder调用阻塞:主线程调用系统服务,比如IntentService、ContentProvider,如果目标进程卡住,主线程也会被拖死。

避坑指南:我曾经在Compose项目中直接在主线程调用了AES加密一个10MB文件,结果5秒后ANR弹窗就出现了。后来我才意识到,加密操作不仅仅是CPU计算,还涉及大量内存分配和GC,低端机秒崩。从那以后,我规定所有加密、压缩、序列化操作必须扔到子线程。

1.3 系统怎么发现你ANR了?内部检测机制拆解

Android系统并不会盯着你的App一直看。它用的是「定时器+超时抛出」的模型。我简单画了个流程图,帮大家理解整个发现过程。

ANR检测机制与常见场景流程 用户操作 / 系统事件 主线程开始处理 快速返回 正常完成 超时未返回 系统定时器触发 ANR弹窗 & 日志 常见触发场景 输入事件 触摸/按键5秒超时 BroadcastReceiver onReceive 10秒超时 Service 生命期回调20秒超时 ContentProvider 请求超时 超时时间从系统接收到事件/回调开始计算,主线程任何阻塞都会导致定时器超时 检测由 system_server 中的 ActivityManagerService 完成

系统具体是怎么工作的?我大致讲一下底层逻辑。

  1. 启动定时器:系统在派发输入事件、执行广播onReceive、启动服务时,会设置一个超时定时器。
  2. 监控主线程:如果主线程在超时时间到达前完成了任务,系统会取消定时器。
  3. 超时触发:如果定时器响了,系统认为主线程死了,就开始收集ANR信息(trace文件、CPU状态等)。
  4. 弹窗+写文件:系统杀掉你的App进程?不会。它只弹对话框。但用户如果选择「关闭」,才会真正杀掉进程。

注意:不同Android版本检测机制有细微差异。我在Android 12上排查过一个ANR,发现系统还会检测长耗时操作导致的Binder调用阻塞,并提前输出警告日志。但核心的定时器模型一直没有变。

1.4 常见ANR场景深度拆解

这部分我结合自己踩过的坑来说,希望能帮大家少走弯路。

输入事件ANR(最常见的ANR)

你想想看,用户点了一个按钮,系统把触摸事件发到主线程,但主线程正在做别的事,5秒后事件还没处理完,ANR就来了。

我遇到最典型的情况:在点击事件中直接调用了同步网络请求。当时是一个登录按钮,代码大概是这样:

// ❌ 错误示例:主线程直接请求
binding.loginButton.setOnClickListener {
    val result = apiService.loginSync(username, password) // 网络请求
    // 网络慢的话,主线程卡死5秒就是ANR
}

正确的做法是用协程或线程池:

// ✅ 正确做法:切到子线程
binding.loginButton.setOnClickListener {
    viewModelScope.launch(Dispatchers.IO) {
        val result = apiService.login(username, password)
        withContext(Dispatchers.Main) {
            // 更新UI
        }
    }
}

广播ANR

广播接收器有个特性:默认情况下,它是在主线程执行的。我见过不少人在onReceive里做数据库查询、文件读写,甚至启动一个长时间的后台任务(比如上传日志)。超时时间是10秒。

避坑指南:我曾经在onReceive里用SharedPreferences写入一个10MB的JSON,结果电量低的手机上写入耗时12秒——广播ANR。后来我改成异步处理,用goAsync()让广播延长到30秒,或者直接启动Service。

// 使用 goAsync() 延长广播处理时间(最多30秒)
override fun onReceive(context: Context?, intent: Intent?) {
    val pendingResult = goAsync()
    thread {
        // 这里可以做一些耗时操作
        // 但要注意,30秒后还会ANR,所以能短则短
        pendingResult.finish()
    }
}

Service ANR

Service的生命周期方法,比如onStartCommand、onBind、onCreate,必须在20秒内执行完毕。如果你在onStartCommand里启动了一个耗时线程,没关系。但如果你直接在onStartCommand里做了阻塞操作,20秒一到,ANR。

场景 超时时间 典型触发原因
输入事件 5秒 主线程阻塞、锁等待、耗时计算
BroadcastReceiver 10秒 onReceive中做I/O、网络、复杂处理
Service 20秒 onStartCommand/onBind等回调阻塞
ContentProvider 因版本而异 query/insert耗时

我个人习惯:在代码里所有涉及主线程耗时操作的地方,都会加一个「超时预警」注释,并且用StrictMode在debug模式下检测。推荐大家在开发环境开启StrictMode的thread policy,它可以帮你抓出很多潜在的ANR风险。

1.5 ANR发生后,系统会留下什么线索?

当系统检测到ANR,它会生成一个trace文件,位于 /data/anr/traces.txt 或 /data/anr/anr_* 。这个文件里包含了所有线程的堆栈信息,尤其是主线程的调用栈。

我曾经根据trace文件定位过一个非常隐蔽的ANR:主线程停在了一个BlockingQueue的take()上,而那个线程因为之前抛了异常没有释放锁。排查了整整两天才找到原因。

所以,学会读trace文件是ANR排查的基本功。后续章节我们会专门讲如何系统化分析trace。