前言
ANR(Application Not Responding)是最让 Android 工程师紧张的 bug 类——用户看着屏幕卡死、给一星差评,5 分钟之内团队就要定位原因。CRASH 还有堆栈能直接看,ANR 要从一份 traces.txt 文件里反推。是时候系统拆一次了。
一、ANR 是什么、什么时候发生
主线程 5 秒是绝大多数 ANR 案例的源头。
二、ANR 后系统做了什么
弹 "App isn't responding" 对话框
写一份
traces.txt到/data/anr/把堆栈、CPU 占用、线程状态记录下来
用户选 "Close" 后杀掉进程
traces.txt 是关键证据,不取它就没法定位。
三、获取 traces.txt
3.1 开发者设备
adb shell ls /data/anr/
# 看到 traces1.txt、traces2.txt 等
adb pull /data/anr/traces1.txt .
3.2 线上用户(使用 APM)
Sentry / Bugly / Firebase Performance 都支持 ANR 自动捕获:
// Firebase Performance 自动捕获
val anrMonitor = FirebasePerformance.getInstance().newTrace("anr_005")
anrMonitor.start()
// ANR 发生时会上报:trace_id + 持续时间
对于国内 Bugly:
CrashReport.setUserId("user-12345")
CrashReport.putUserData(this, "channel", "huawei")
// Bugly 自动捕获 ANR + 用户维度
线上 ANR 排查优先查 APM 控制台。
3.3 自建 ANR Watchdog
class AnrWatchdog(private val timeoutMs: Long = 5000L) : Thread() {
private val mainHandler = Handler(Looper.getMainLooper())
@Volatile private var ticked = false
override fun run() {
super.run()
while (!isInterrupted) {
ticked = false
mainHandler.post { ticked = true }
try {
Thread.sleep(timeoutMs)
} catch (e: InterruptedException) {
return
}
if (!ticked) {
onAnrDetected()
}
}
}
private fun onAnrDetected() {
val mainStack = Looper.getMainLooper().thread.stackTrace
val stack = buildString {
mainStack.forEach { appendLine("\tat $it") }
}
crashReporter.reportAnr(stack)
}
}
5 秒没回应就抓主线程栈上报。
四、traces.txt 文件结构
----- pid 12345 at 2026-08-24 10:23:42 -----
Cmd line: com.example.app
"DALVIK THREADS" (mutexes: tll=0 tsll=0 gsll=0 msll=0 hwll=0)
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 ucsCount=1 flags=1 obj=0x7328b538 self=0x...
| sysTid=12345 nice=0 cgrp=default sched=0/0
| state=S schedstat=(...) handle=0x...
| stack=0x72c45000-0x72c47000 stackSize=8192KB
| held mutexes=
at java.lang.Thread.sleep(Native method)
- waiting on <0x12345678> (a java.lang.Object)
at com.example.app.network.NetworkHandler.fetch(NetworkHandler.kt:42)
at com.example.app.ui.MainActivity.onCreate(MainActivity.kt:30)
...
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:6785)
"OkHttp Dispatcher" prio=5 tid=15 TIMED_WAIT
...
关键字段:
线程名:main / OkHttp Dispatcher / Thread-15
tid = 1 主线程
state Sleeping / RUNNABLE / BLOCKED / WAITING
stack 完整栈
held mutexes 当前持有的锁
主线程往往是 BLOCKED / WAITING 在某个锁上。
五、读 traces.txt 的实战套路
5.1 先看主线程状态
"main" prio=5 tid=1 BLOCKED
主线程有四种状态,含义不同:
BLOCKED / WAITING 多半是死锁、缺锁竞争。
5.2 反向定位卡顿代码
栈倒着看:
at com.example.app.network.NetworkHandler.fetch(NetworkHandler.kt:42)
at com.example.app.ui.MainActivity.onCreate(MainActivity.kt:30)
NetworkHandler.fetch 在第 42 行卡住。打开源码看那段代码,多数是网络请求 / 数据库同步 / 文件 IO 在主线程。
5.3 锁竞争场景
"main" prio=5 tid=1 BLOCKED
at com.example.app.sync.DataSync.sync(DataSync.kt:84)
- waiting to lock <0x1234> (a com.example.app.sync.SyncLock)
"Thread-15" prio=5 tid=15 TIMED_WAIT
at java.lang.Object.wait(Native method)
- waiting on <0x1234> (a com.example.app.sync.SyncLock)
held by main
主线程持有锁,等在 SyncLock;但还有一个线程等不到这把锁。一旦主线程被切到 BLOCKED,整个流程就死锁。
5.4 看 CPU 占用
schedstat=(...) handle=... 给出 CPU 占用情况:
六、AnrWatchDog + 现场录制的实战方案
class AnrDetector {
private val mainHandler = Handler(Looper.getMainLooper())
fun detect(): String? {
val watchdogToken = Object()
var stackTrace: String? = null
mainHandler.post { synchronized(watchdogToken) { watchdogToken.notifyAll() } }
val waitMs = 5000L
synchronized(watchdogToken) {
try {
val start = System.currentTimeMillis()
watchdogToken.wait(waitMs)
val elapsed = System.currentTimeMillis() - start
if (elapsed >= waitMs - 100) {
stackTrace = captureMainStackTrace()
}
} catch (e: InterruptedException) {
return null
}
}
return stackTrace
}
private fun captureMainStackTrace(): String {
val mainThread = Looper.getMainLooper().thread
return buildString {
mainThread.stackTrace.forEach { appendLine("\tat $it") }
}
}
}
把采集到的栈发到日志:
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
val stack = AnrDetector().detect()
if (stack != null) {
Logger.error("ANR_DETECTED", stack)
}
}
}
七、修复模式(按场景)
7.1 主线程网络请求
// 错误
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
super.onCreate(...)
val result = HttpURLConnection("https://api.example.com/v1/data").inputStream
// 5 秒没返回 → ANR
}
}
// 正确
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
super.onCreate(...)
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
apiClient.fetch()
}
}
}
}
7.2 主线程数据库查询
// 错误
fun load(): List<Order> {
return db.rawQuery("SELECT * FROM order", null).use { c ->
mutableListOf<Order>().also { list ->
while (c.moveToNext()) list.add(toOrder(c))
}
}
}
// 正确:Room 内置 IO 调度
@Dao
interface OrderDao {
@Query("SELECT * FROM order")
suspend fun loadAll(): List<Order>
}
7.3 SharedPreferences 的 commit()
// 错误:commit 同步写入磁盘
sp.edit().putString("key", "value").commit()
// 正确:apply 异步写入
sp.edit().putString("key", "value").apply()
7.4 Bitmap 在主线程解码
// 错误
val bm = BitmapFactory.decodeFile(path, options)
// 正确:在工作线程
val bm = withContext(Dispatchers.IO) {
BitmapFactory.decodeFile(path, options)
}
7.5 大 Object 序列化
// 错误:JSON 在主线程
val text = JSONObject(map).toString()
// 正确
val text = withContext(Dispatchers.Default) {
JSONObject(map).toString()
}
7.6 第三方 SDK 在主线程初始化
// 错误
override fun onCreate() {
super.onCreate()
SomeCommercialSDK.init(this) // 内部阻塞 4 秒
}
// 正确:分阶段或移到 IO
override fun onCreate() {
super.onCreate()
lifecycleScope.launch(Dispatchers.IO) {
delay(2000) // 延后启动
SomeCommercialSDK.init(this@App)
}
}
八、ANR 预防策略
// build.gradle.kts
android {
lintOptions {
checkAllWarnings = true
abortOnError = true
}
}
自定义 Lint 规则:
class MainThreadIOUsageDetector : Detector(), SourceCodeScanner {
override fun visitMethodCall(context: JavaContext, call: PsiMethodCall, ...) {
if (call.methodName == "readObject" || call.methodName == "loadFromAssets") {
context.report(ISSUE, call, context.getNameLocation(call),
"Avoid I/O on main thread")
}
}
}
九、案例分析
真实案例:从 0.5% ANR 率降到 0.02%
排查发现:
后端 API 偶发 8-12 秒响应
主线程用了 RxJava
blockingSubscribe()等待结果
修改方案:
改成 callback / coroutine 异步调用
加超时机制(5 秒 timeout),失败弹 Toast
后端响应超过 5 秒时上报"慢响应" → 后端优化
一周后数据:
ANR 率从 0.5% → 0.02%
启动时间从 4 秒 → 2.5 秒
Crash 率顺带降了 5%
小结
ANR 的核心是"主线程被阻塞"。修复路径有 4 步:1. 取 traces.txt;2. 找到主线程阻塞点;3. 反向溯源代码位置;4. 把同步调用改成异步。预防靠 Lint + Code Review + 灰度 + APM。
下一篇聊 Crash 监控与稳定性工程——ANR 之外,Crash 是另一类难以追踪的稳定性问题。
参考阅读
Android 官方:ANR 排查指南
Google:StrictMode 检测主线程慢操作
阿里:Android ANR 监控与处理