Android ANR 排查实战:主线程被 Binder 回调拖死时怎么快速止血

作者: Android学习网 分类: Android基础知识 发布时间: 2026-03-23 16:31

## Android ANR 排查先别急着看表面报错

Android ANR 排查 最容易把人带偏的地方,是团队一看到 Input dispatching timed out 就条件反射去翻页面层日志。真正难的是主线程为什么会被拖住,以及卡顿究竟发生在 UI 计算、磁盘 IO、锁竞争,还是 Binder 回调链路里。如果没有先把线程阻塞点钉死,后面做的优化大概率只是换一种方式继续卡。

我在现场排 Android ANR 排查 时,先做三件事:第一,把最近一次 ANR 前后 10 秒的主线程堆栈和 binder 线程堆栈一起抓出来;第二,把主线程里所有可能跨进程调用的入口列清楚;第三,确认是不是某个同步接口在页面关键路径上被直接调用。很多项目的问题不在“没有异步”,而在“以为自己已经异步了”,结果回调最终又切回主线程同步等待。

## Android ANR 排查先按这条顺序止血

这类问题别上来就大改架构,先走最短止血链:先观测、再切线程、最后压回归。先观测的目的是知道卡顿发生在哪个阶段,而不是靠猜。再切线程是把最危险的同步 Binder 调用和磁盘读写移出主线程。最后压回归,是为了防止你把 ANR 修掉之后,又引入状态错乱或者二次回调丢失。

如果主线程上同时存在 IPC、数据库、JSON 解析三种重活,就不要一口气全改。最稳的做法是先把 Binder 回调和本地持久化拆开,把能缓存的结果先缓存,把必须进主线程的部分压缩到只剩 UI 状态更新。Android ANR 排查 的关键不是会不会起协程,而是能不能把主线程只保留不可替代的那一点工作。

## Android ANR 排查核心代码

1. 先看高风险写法

class DeviceInfoRepository(
    private val remoteService: IRemoteService
) {
    fun loadBlocking(): String {
        return remoteService.fetchInfo()
    }
}

class MainViewModel(
    private val repository: DeviceInfoRepository
) : ViewModel() {
    fun onPageEnter() {
        val result = repository.loadBlocking()
        println(result)
    }
}

2. 改成可控线程切换

class SafeDeviceInfoRepository(
    private val remoteService: IRemoteService,
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
    suspend fun load(): String = withContext(dispatcher) {
        remoteService.fetchInfo()
    }
}

class SafeMainViewModel(
    private val repository: SafeDeviceInfoRepository
) : ViewModel() {
    private val state = MutableStateFlow("idle")

    fun onPageEnter() {
        viewModelScope.launch {
            state.value = "loading"
            val result = repository.load()
            state.value = result
        }
    }
}

3. 给主线程补守门员

class MainThreadGuard {
    fun install() {
        StrictMode.setThreadPolicy(
            StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork()
                .penaltyLog()
                .build()
        )
    }
}

## Android ANR 排查常见坑

第一个坑是只盯业务日志,不抓系统层证据。ANR 不是普通异常,很多时候应用自己根本来不及把关键日志落下来,你必须结合 races.txt、logcat、dumpsys 一起看。

第二个坑是误以为“异步入口 + 同步回调”也算异步。很多团队把请求封装进协程、线程池或 Rx 链路后,后面又在回调里 lockingGet、join 或等待 CountDownLatch,最后还是把主线程卡死。

## Android ANR 排查报错与收口

1. 首屏偶发卡死

fun markCost(tag: String, block: () -> Unit) {
    val start = SystemClock.elapsedRealtime()
    block()
    val cost = SystemClock.elapsedRealtime() - start
    Log.d("CostTrace", " cost=ms")
}

2. 页面返回再进入更容易 ANR

class PageTaskController {
    private var currentJob: Job? = null

    fun replace(job: Job) {
        currentJob?.cancel()
        currentJob = job
    }

    fun clear() {
        currentJob?.cancel()
        currentJob = null
    }
}

## Android ANR 排查工具与验证

adb shell am stack list
adb shell dumpsys activity processes | findstr /I anr
adb shell dumpsys binder_calls_stats
adb logcat -d | findstr /I "ANR Input dispatching StrictMode"

最终验证时,不要只测“问题没复现”,而要测这三件事:主线程关键路径里是否还存在同步 Binder 或磁盘 IO;异步改造后状态是否有倒退、闪动、重复提交;页面频繁进出、弱网、后台恢复时是否仍然稳定。

## Android ANR 排查结论与下一步

Android ANR 排查 真正拉开差距的地方,不在于能不能背出几条优化建议,而在于你能不能把卡死线程、等待对象和调用入口快速钉死。下一步建议很明确:先把所有可能的同步 Binder 和主线程重活列清单,再给高风险 API 补线程契约、超时和监控。

## Android ANR 排查可运行片段

object ThreadCheck {
    fun assertNotMain(tag: String) {
        check(Looper.myLooper() != Looper.getMainLooper()) {
            " should not run on main thread"
        }
    }
}

suspend fun fetchOnIo(service: IRemoteService): String = withContext(Dispatchers.IO) {
    ThreadCheck.assertNotMain("fetchOnIo")
    service.fetchInfo()
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注