Administrator
发布于 2026-08-26 / 26 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 85 篇:ANR 排查完全指南:从发生到定位到修复

前言

ANR(Application Not Responding)是最让 Android 工程师紧张的 bug 类——用户看着屏幕卡死、给一星差评,5 分钟之内团队就要定位原因。CRASH 还有堆栈能直接看,ANR 要从一份 traces.txt 文件里反推。是时候系统拆一次了。

一、ANR 是什么、什么时候发生

类型

触发条件

系统提示

主线程 5 秒无响应

Activity onCreate、onResume、事件分发

"App isn't responding" 弹窗

BroadcastReceiver 10 秒未结束

onReceive 未返回

弹窗

Service 20 秒未调用 stopSelf

前台服务停滞

弹窗(部分 OEM)

Input 事件 5 秒未处理

触摸/按键

弹窗

ContentProvider publish 超时

publish 阻塞

警告

主线程 5 秒是绝大多数 ANR 案例的源头。

二、ANR 后系统做了什么

  1. 弹 "App isn't responding" 对话框

  2. 写一份 traces.txt 到 /data/anr/

  3. 把堆栈、CPU 占用、线程状态记录下来

  4. 用户选 "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

主线程有四种状态,含义不同:

状态

含义

RUNNABLE

正在执行

BLOCKED

等待锁

WAITING / TIMED_WAIT

等待信号

Sleeping

在 Thread.sleep

Native

在 native 代码中

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 占用情况:

占用率

含义

> 80%

主线程被 CPU 密集型任务阻塞

< 30%

主线程在等外部资源(IO/锁)

≈ 0%

主线程被 OS 切走

六、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 预防策略

阶段

措施

编码

强制 coroutine / Handler 异步

Code Review

检查任何 suspend/runBlocking/Thread.sleep 路径

Lint

自定义规则禁主线程 IO

单测

加 StressTest 在 1000 次循环跑临界场景

灰度

1% 用户先用,监控 ANR 率 < 0.05% 再放大

APM

实时报警,5 分钟内响应

// 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() 等待结果

修改方案:

  1. 改成 callback / coroutine 异步调用

  2. 加超时机制(5 秒 timeout),失败弹 Toast

  3. 后端响应超过 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 监控与处理


评论