Android ANR 排查实战:主线程被 Binder 回调拖死时怎么快速止血
## 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()
}
