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一直看。它用的是「定时器+超时抛出」的模型。我简单画了个流程图,帮大家理解整个发现过程。
系统具体是怎么工作的?我大致讲一下底层逻辑。
- 启动定时器:系统在派发输入事件、执行广播onReceive、启动服务时,会设置一个超时定时器。
- 监控主线程:如果主线程在超时时间到达前完成了任务,系统会取消定时器。
- 超时触发:如果定时器响了,系统认为主线程死了,就开始收集ANR信息(trace文件、CPU状态等)。
- 弹窗+写文件:系统杀掉你的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。