
📚 读者点单·端午投票系列 · 第7/10篇
✅ 第1篇:Android 性能治理的「全景图」
✅ 第2篇:Android 启动优化实战
✅ 第3篇:Compose 与传统 View 混用的 12 个真实坑
✅ 第4篇:Android 内存治理实战
✅ 第5篇:Token 节省专题
✅ 第6篇:掉帧全链路拆解
👉 第7篇:ANR 治理实战(本篇)
⏳ 第8篇:AI × Android 端侧落地
⏳ 第9篇:Android 包体积治理
⏳ 第10篇:系列复盘
性能稳定性的最后一公里
线上收到一条 ANR 上报,打开 traces.txt 一看:主线程卡在 nativePollOnce——这大概是最容易让人「松口气」又「误判」的情况了。你以为主线程空闲?不,它可能刚处理完一大坨消息、系统正好在那一刻抓了快照。ANR 归因远不是「主线程栈顶在干什么」这么简单,它是一个需要交叉验证多份日志的侦探游戏。
这篇是系列第 7 篇,聊的是 Android 性能治理中公认最棘手的一环——ANR 治理。我会从系统检测机制讲起,手把手拆解 Trace 文件的三段式解读,然后聊三类高频根因(消息积压、Binder 超时、锁竞争)的实战定位,最后带你看 2026 年新冒出来的 AI 辅助分析工具链。
📰 科技要闻
• OpenAI 正式发布 GPT-5.6,多模态推理能力大幅升级,上下文窗口突破 200 万 token,定价比 GPT-5 降低 40%
• DeepSeek 开启大规模招聘,岗位覆盖系统工程师、RLHF 研究员、推理优化等方向,传闻估值突破 600 亿美元
• 黄金再度跌破 4000 美元/盎司关口,市场预期美联储 7 月维持利率不变,资金回流美股科技板块
• 苹果因关税涨价引发山姆会员店代购潮,iPhone 17 系列国行价格上调 8-12%
一、ANR 的 4 种触发场景
在开始读 Trace 之前,先搞清楚系统「什么时候」会判你 ANR。很多人只记得「5 秒」,但实际上有 4 种不同的超时阈值:
触发场景 | 超时时间 | 典型根因 |
|---|---|---|
Input dispatching | 5s | 主线程阻塞无法响应触摸/按键 |
BroadcastReceiver | 前台10s / 后台60s | onReceive 做了耗时操作 |
Service | 前台20s / 后台200s | onCreate/onStart 耗时 |
ContentProvider | 10s | publish 超时(冷启动高发) |
其中 Input dispatching 是线上最常见的类型,占比通常在 70% 以上。系统侧检测逻辑大致是这样的:InputDispatcher 分发事件时启动一个计时器,如果 5 秒内没收到 App 的 finishInputEvent 回执,就触发 ANR 流程——dump 所有线程栈、写 traces.txt、弹对话框。
关键认知:ANR 的 Trace 是「案发后的现场快照」,不是「案发瞬间的录像」。主线程可能在前 4.9 秒做了密集操作,dump 的那一刻已经回到空闲态——这就是 nativePollOnce 误判的典型场景。
二、ANR Trace 三段式解读
每次 ANR 发生,系统会在 /data/anr/traces.txt 写入所有进程的线程栈。一份 Trace 的核心信息分三段来读:
第一段:Header(进程元信息)
----- pid 12345 at 2026-06-27
09:15:03 -----
Cmd line: com.example.app
Build fingerprint:
'google/raven/raven:14'
ABI: 'arm64'
Build type: userdebug
Suspend all histogram:
1 10 100 1000这里要关注 at 后面的时间戳——它是系统 dump 的时刻,不是 ANR 开始的时刻。真正的 ANR 起始时间要去 EventLog 里找 am_anr 条目。
第二段:Thread State(线程状态)
"main" prio=5 tid=1
BLOCKED
| group="main"
count=1
| held mutexes=
| waiting to lock
0x0a2b3c4d
(a java.lang.Object)
| held by thread 15线程状态是最直接的信号。几种关键状态的解读:
状态 | 含义 | 常见根因 |
|---|---|---|
BLOCKED | 等待锁 | 锁竞争 / 死锁 |
WAITING | 无限等待 | wait() / join() |
RUNNABLE | 正在执行 | CPU 密集 / IO |
Native | 在 native 层 | Binder / IO / JNI |
第三段:Stack Trace(调用栈)
at android.os
.BinderProxy
.transactNative(
Native Method)
at android.os
.BinderProxy
.transact(
BinderProxy.java:571)
at android.app
.IActivityManager
.broadcastIntent(
IActivityManager.java:5043)
at com.example.app
.SyncManager
.notifyDataChanged(
SyncManager.kt:127)这份栈从下往上读:App 代码调了 notifyDataChanged,里面发了广播,走到 Binder IPC 的 transactNative 卡住了——这是一个典型的 Binder 调用超时场景。但如果你只看栈顶的 transactNative,很容易归因为「系统问题」而忽略了自己代码在主线程发起的 IPC。
实战技巧:读 ANR Trace 时永远「从下往上」看调用栈,先找到你自己的代码(com.xxx),那才是根因入口。再结合线程状态判断卡在什么类型的操作上。
三、三类高频根因:定位实战
读了上百份线上 ANR Trace 之后,我把根因分成三大类。每类的定位思路和修复策略完全不同,搞混了会白费力气:
根因一:MessageQueue 积压
主线程不是「一直在做一件慢事」,而是「连续收到太多消息,每条都不慢,但排队排爆了」。典型场景:列表快速滑动时大量 notifyDataSetChanged 堆积,或者 LiveData 短时间内多次 postValue 触发连锁 Observer 回调。
Trace 的特征是:主线程状态为 RUNNABLE,栈顶在 nativePollOnce 或某个 Handler 回调里——看起来「没问题」,但 EventLog 里 dvm_lock_sample 密集出现。
治理策略是消息分级 + 限流:
class ThrottledHandler(
looper: Looper,
private val windowMs:
Long = 100
) : Handler(looper) {private var lastDispatch =
0L
private var pending:
Runnable? = nullfun postThrottled(
r: Runnable
) {
pending = r
val now =
SystemClock
.uptimeMillis()
val delay =
windowMs -
(now - lastDispatch)
if (delay <= 0) {
dispatch()
} else {
postDelayed(
::dispatch,
delay
)
}
}private fun dispatch() {
lastDispatch =
SystemClock
.uptimeMillis()
pending?.run()
pending = null
}
}核心思路:同类消息在时间窗口内只保留最后一条,丢弃中间的。100ms 窗口对 UI 刷新几乎无感知差异,但能把消息数降 80% 以上。
根因二:Binder IPC 超时
这是低端机 ANR 的头号杀手。主线程调了一个系统服务(获取电量、查询包信息、注册广播),对面 system_server 忙着处理其他 App 的请求,你的 Binder 调用就卡在那里等着。
Trace 特征非常明显:主线程状态 Native,栈顶 android.os.BinderProxy.transactNative。但你需要往下看是谁发起的——往往是一个看起来人畜无害的 API 调用:
// 这些看起来无害的 API,
// 底层都是 Binder IPC:
context.getPackageManager()
.getInstalledPackages(0)Settings.System
.getInt(
resolver,
SCREEN_BRIGHTNESS
)context
.registerReceiver(
receiver, filter
)治理策略是「主线程零 Binder」原则——所有可能走 IPC 的调用都搬到子线程:
// 用 StrictMode 检测主线程
// Binder 调用(Debug 包开启)
StrictMode.setThreadPolicy(
StrictMode
.ThreadPolicy
.Builder()
.detectCustomSlowCalls()
.penaltyLog()
.build()
)// 封装异步 Binder 工具
suspend fun safeBinderCall(
block: () -> T
): T = withContext(
Dispatchers.IO
) {
block()
}根因三:锁竞争与死锁
Trace 里最好认的一种——主线程状态 BLOCKED,并且明确写了 waiting to lock 0x... held by thread X。找到 thread X 的栈,看它在做什么:如果它也在等另一个锁(由主线程持有),恭喜,死锁。
实战中更常见的是「非死锁但长时间持锁」:子线程拿着 synchronized 块做数据库写入,主线程恰好也需要读同一份数据。治理方向:
• 用 ReadWriteLock 替代 synchronized,读不互斥
• 缩小锁粒度:只锁 map.put,不锁整个方法
• 用 ConcurrentHashMap / StateFlow 等无锁方案
• 最狠:主线程永远不等锁——读缓存副本,写走异步
四、Service / BroadcastReceiver 的 ANR 高发场景
除了 Input dispatching,Service 和 BR 的 ANR 在某些 App 里占比不低。说几个我踩过的坑:
场景 A:前台 Service onCreate 超时
调了 startForegroundService() 但没在 5s 内调 startForeground()。Android 12+ 这个限制更严,而且 crash 不会留给你弹 ANR 对话框的机会,直接 ForegroundServiceDidNotStartInTimeException。
场景 B:BR onReceive 里做同步 IO
BroadcastReceiver 的 onReceive 跑在主线程,前台 BR 只有 10s。很多人在里面发 HTTP 请求或写文件——这是在赌:网络快的时候不出事,慢的时候必 ANR。正确做法是用 goAsync() 拿到 PendingResult,然后在子线程处理:
class SafeReceiver :
BroadcastReceiver() {override fun onReceive(
ctx: Context,
intent: Intent
) {
val pending =
goAsync()
CoroutineScope(
Dispatchers.IO
).launch {
try {
doHeavyWork(
intent
)
} finally {
pending
.finish()
}
}
}
}场景 C:ContentProvider 冷启动阻塞
App 冷启动时所有 ContentProvider 的 onCreate 在主线程依次执行。第三方 SDK(Firebase、微信 SDK)喜欢注册 Provider 做初始化——如果它们的 onCreate 耗时叠加超过 10s,直接 ANR。治理方向就是我们第二篇讲过的 App Startup 懒加载。
五、2026 新趋势:AI 辅助 ANR 分析
说实话,读 Trace 是个手艺活。刚入行的同学经常看到 nativePollOnce 就报「没问题」,而老手能从 EventLog 时间线、CPU 负载、内存水位综合判断。但 2026 年有个明显趋势:LLM 开始能做这件事了。
llm-anr:多源证据工程 + LLM 归因
6 月刚开源的 llm-anr 项目,思路很清晰:先把 trace、EventLog、logcat、AnrManager dump、meminfo、kernel log 等多源原始材料,按 ANR 事件隔离成「证据包」,然后交给 LLM Agent 生成可审计的分析结论。
ANR 事件触发
↓
多源日志采集
traces + EventLog + logcat + meminfo
↓
证据工程(按事件隔离)
↓
LLM Agent 分析
↓
✅ 输出 → 可审计的归因报告 + 修复建议
它解决的核心问题是:线上每天几百上千条 ANR,人工一条条看不现实。LLM 能批量处理,而且「证据工程」这一步保证了可审计性——你可以回溯 LLM 的结论是基于哪些日志段落得出的。
SmartPerfetto:AI Agent 解析 Perfetto Trace
androidperformance.com 4 月发的深度文,讲的是用 AI Agent 架构解析 Perfetto trace。和 llm-anr 不同的是,它的分析对象是整个 Perfetto 文件(包含调度、内存、CPU 等多维信息),更适合做全局性能归因而不仅仅是 ANR。Agent 架构让它能“多步推理”——先看 CPU 调度、再看线程竞争、最后归因。
六、线上监控闭环:4 种方案对比
定位能力有了,还得解决「怎么知道发生了 ANR」。原生的 ApplicationExitInfo 是 Android 11 提供的官方接口,但更早版本没法用。常用的 4 种方案:
方案 | 原理 | 优势 | 局限 |
|---|---|---|---|
FileObserver | 监听 /data/anr/ 目录文件创建 | 实现简单 | Android 11+ 权限受限 |
SIGQUIT 捕获 | 拦截系统发给 App 的 signal 3 | 可获取完整 Trace | 需 Native hook,有兼容风险 |
ApplicationExitInfo | App 重启后查询上次退出原因 | 官方 API,稳定可靠 | Android 11+ only |
主线程心跳 | 子线程定时往主线程 post,检测回执延迟 | 全版本兼容,无侵入 | 只能检测主线程卡顿,非精确 ANR |
我的建议是组合使用:ApplicationExitInfo 做精确检测(Android 11+),主线程心跳做全版本补充,再配合实时栈采样获取现场数据:
// 主线程心跳监控(简化版)
class AnrWatchdog(
private val
thresholdMs: Long =
5000
) : Thread("AnrWatchdog") {private val handler =
Handler(
Looper
.getMainLooper()
)
@Volatile
private var tick =
0Loverride fun run() {
while (!isInterrupted) {
tick =
SystemClock
.uptimeMillis()
val expected = tick
handler.post {
tick =
0L
}
Thread.sleep(
thresholdMs
)
if (tick ==
expected) {
// 主线程未响应
dumpStack()
reportAnr()
}
}
}
}七、小结与下一篇预告
ANR 治理的核心思路其实就三步:
• 发现:多维监控(ApplicationExitInfo + 心跳 + 栈采样)
• 定位:Trace 三段式解读 + EventLog 交叉验证
• 修复:消息限流 / Binder 异步化 / 无锁方案
2026 年的 LLM 工具链让「定位」这一步的效率提升了一个数量级,但「发现」和「修复」依然靠工程能力。所以 Trace 解读这个基本功,还是得练。
下一篇“读者点单·08”,我们进入一个全新方向——AI × Android 端侧落地:MLC-LLM、MediaPipe、厂商 NPU 这些方案的真实工程体验。如果你在做端侧大模型,别错过。
参考材料: • llm-anr:多源证据工程 + LLM 辅助分析 (codemx.cn, 2026-06) • ANR 治理 2026:实时栈采样 + Looper 监听 + SIGQUIT 信号捕获 (腾讯云开发者社区, 2026-06-24) • SmartPerfetto AI Agent 架构深潜 (androidperformance.com, 2026-04) • Android ANR 深度治理:从主线程卡顿根因到 Trace 全链路分析 (xckevin.github.io, 2026-04)
📚 读者点单·端午投票系列 · 第7/10篇
基于端午《聊聊学习节奏》评论区读者票选生成的系列文章
✅ 第1篇:Android 性能治理的「全景图」
✅ 第2篇:Android 启动优化实战
✅ 第3篇:Compose 与传统 View 混用的 12 个真实坑
✅ 第4篇:Android 内存治理实战
✅ 第5篇:Token 节省专题
✅ 第6篇:掉帧全链路拆解
👉 第7篇:ANR 治理实战(本篇)
⏳ 第8篇:AI × Android 端侧落地
⏳ 第9篇:Android 包体积治理
⏳ 第10篇:系列复盘