线上最难受的稳定性问题,往往不是"某个空指针崩了"这么简单。更真实的情况是:某个版本刚灰度,Crash 率往上跳,ANR 也跟着涨;用户评论里说"打开就闪退""点一下没反应""页面卡死";你打开日志平台,Java 崩溃、Native 崩溃、OOM、主线程阻塞、启动失败混在一起,像一团线。
一句话结论: 崩溃优化不是到处加 try-catch,应用稳定性也不是只盯 Crash 率;真正可持续的做法,是把 Crash、ANR、OOM、Native Crash、卡顿和启动失败拆成不同故障类型,建立采集、归因、止血、修复、验证和回归防线。
本文以 Android 应用开发侧的稳定性治理为主,重点讲 Java Crash、Native Crash、ANR、OOM 和线上闭环。Android Vitals 的阈值、ANR 诊断、ApplicationExitInfo 等信息按 2026-10-09 查询的官方文档整理;具体数据口径和 Play Console 展示可能随 Google Play 政策更新,落项目时以当前控制台和官方文档为准。
0. 先把稳定性问题分清楚
很多团队一开会就说"线上不稳定",但这个词太大了。你要先问一句:到底是哪一种不稳定?
| 类型 | 用户感受 | 常见根因 | 常见证据 |
|---|---|---|---|
| Java Crash | 闪退 | 空指针、越界、非法状态、线程异常 | Java 堆栈、设备信息、业务现场 |
| Native Crash | 闪退 | C/C++ 越界、空指针、SIGSEGV、SIGABRT | tombstone、信号、native 堆栈 |
| ANR | 卡死、点不动 | 主线程阻塞、锁等待、Binder 卡住、I/O 卡住 | ANR traces、主线程堆栈、Perfetto |
| OOM | 闪退或进程被杀 | 图片过大、泄漏、缓存失控、native 内存膨胀 | OOM 堆栈、heap dump、meminfo |
| 启动失败 | 打不开、白屏后退出 | Provider、Application、首屏初始化异常 | 启动埋点、Crash 堆栈、ANR 信息 |
| 卡顿 | 不流畅 | 主线程长任务、布局过重、频繁 GC | Frame metrics、Perfetto、卡顿堆栈 |
这几类问题不能用一套方法硬套。Crash 要看异常和堆栈,ANR 要看主线程为什么没响应,OOM 要看内存增长和对象持有,Native Crash 要看信号和 tombstone。
#mermaid-svg-85nGaJFjPoNHgru1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-85nGaJFjPoNHgru1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-85nGaJFjPoNHgru1 .error-icon{fill:#552222;}#mermaid-svg-85nGaJFjPoNHgru1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-85nGaJFjPoNHgru1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-85nGaJFjPoNHgru1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-85nGaJFjPoNHgru1 .marker.cross{stroke:#333333;}#mermaid-svg-85nGaJFjPoNHgru1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-85nGaJFjPoNHgru1 p{margin:0;}#mermaid-svg-85nGaJFjPoNHgru1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-85nGaJFjPoNHgru1 .cluster-label text{fill:#333;}#mermaid-svg-85nGaJFjPoNHgru1 .cluster-label span{color:#333;}#mermaid-svg-85nGaJFjPoNHgru1 .cluster-label span p{background-color:transparent;}#mermaid-svg-85nGaJFjPoNHgru1 .label text,#mermaid-svg-85nGaJFjPoNHgru1 span{fill:#333;color:#333;}#mermaid-svg-85nGaJFjPoNHgru1 .node rect,#mermaid-svg-85nGaJFjPoNHgru1 .node circle,#mermaid-svg-85nGaJFjPoNHgru1 .node ellipse,#mermaid-svg-85nGaJFjPoNHgru1 .node polygon,#mermaid-svg-85nGaJFjPoNHgru1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-85nGaJFjPoNHgru1 .rough-node .label text,#mermaid-svg-85nGaJFjPoNHgru1 .node .label text,#mermaid-svg-85nGaJFjPoNHgru1 .image-shape .label,#mermaid-svg-85nGaJFjPoNHgru1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-85nGaJFjPoNHgru1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-85nGaJFjPoNHgru1 .rough-node .label,#mermaid-svg-85nGaJFjPoNHgru1 .node .label,#mermaid-svg-85nGaJFjPoNHgru1 .image-shape .label,#mermaid-svg-85nGaJFjPoNHgru1 .icon-shape .label{text-align:center;}#mermaid-svg-85nGaJFjPoNHgru1 .node.clickable{cursor:pointer;}#mermaid-svg-85nGaJFjPoNHgru1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-85nGaJFjPoNHgru1 .arrowheadPath{fill:#333333;}#mermaid-svg-85nGaJFjPoNHgru1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-85nGaJFjPoNHgru1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-85nGaJFjPoNHgru1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-85nGaJFjPoNHgru1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-85nGaJFjPoNHgru1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-85nGaJFjPoNHgru1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-85nGaJFjPoNHgru1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-85nGaJFjPoNHgru1 .cluster text{fill:#333;}#mermaid-svg-85nGaJFjPoNHgru1 .cluster span{color:#333;}#mermaid-svg-85nGaJFjPoNHgru1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-85nGaJFjPoNHgru1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-85nGaJFjPoNHgru1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-85nGaJFjPoNHgru1 .icon-shape,#mermaid-svg-85nGaJFjPoNHgru1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-85nGaJFjPoNHgru1 .icon-shape p,#mermaid-svg-85nGaJFjPoNHgru1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-85nGaJFjPoNHgru1 .icon-shape .label rect,#mermaid-svg-85nGaJFjPoNHgru1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-85nGaJFjPoNHgru1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-85nGaJFjPoNHgru1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-85nGaJFjPoNHgru1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线上稳定性问题
Crash
ANR
OOM
卡顿
启动失败
Java 异常
Native 信号
主线程阻塞
内存失控
帧耗时过长
Provider 和 Application
我的经验是,稳定性治理最怕"混着修"。比如 ANR 上升时,只看 Crash 平台;OOM 频发时,只在业务代码外面套 try-catch;Native Crash 没符号表,却硬猜 Java 层代码。这种处理会很累,而且很容易修错。
1. Android Vitals 先给我们一个线上口径
Google Play 的 Android Vitals 会跟踪影响用户体验的稳定性指标。官方文档里,用户感知 Crash 率和用户感知 ANR 率都有 bad behavior 阈值。
按 2026-10-09 查询到的官方说明:
| 指标 | 整体 bad behavior 阈值 | 单设备型号 bad behavior 阈值 | 说明 |
|---|---|---|---|
| 用户感知 Crash 率 | 1.09% | 8% | 统计每天至少遇到一次用户感知 Crash 的日活用户占比 |
| 用户感知 ANR 率 | 0.47% | 8% | 统计每天至少遇到一次用户感知 ANR 的日活用户占比 |
这里的"用户感知"很关键。不是每个后台异常都同等重要,真正影响评分和口碑的,通常是用户正在使用时发生的闪退、卡死、启动失败。
但项目内部不要只看一个总 Crash 率。更有用的指标至少包括:
| 指标 | 为什么要看 |
|---|---|
| Crash 率 | 版本是否稳定的基础指标 |
| ANR 率 | 交互卡死,用户体感很差 |
| 启动 Crash 率 | 影响打开 App,是最高优先级 |
| 启动 ANR 率 | 常由 Provider、Application、首屏初始化引起 |
| 影响用户数 | 评估事故面,比单纯次数更接近用户损伤 |
| Top 问题占比 | 判断是少数大问题还是很多小问题 |
| 新增问题数 | 识别版本引入的新坑 |
| 修复后回归率 | 看治理是否真的闭环 |
稳定性治理不是"崩了再修"。它应该像性能一样有基线,有门禁,有灰度,有回滚。
2. Java Crash:别把 try-catch 当护身符
Java Crash 最常见,也最容易被误解。比如:
kotlin
val name = user.profile.name
线上某个用户的 profile 为空,就直接 NullPointerException。这种问题看起来很简单,但如果发生在启动页、首页、支付结果页,就可能变成大面积事故。
Java Crash 的采集核心是默认未捕获异常处理器:
kotlin
// 简化教学版:说明链路,不是完整生产实现。
class CrashReporter(
private val previous: Thread.UncaughtExceptionHandler?
) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, throwable: Throwable) {
val report = buildReport(thread, throwable)
writeToLocalFile(report)
uploadIfPossible(report)
previous?.uncaughtException(thread, throwable)
?: kotlin.system.exitProcess(10)
}
}
fun installCrashReporter() {
val previous = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(CrashReporter(previous))
}
这里要注意两件事。
第一,采集器不是让 App "崩了也继续跑"。未捕获异常通常说明当前状态已经不可信。你可以记录现场,但不要盲目吞掉异常继续执行,否则可能从一次明确崩溃变成数据错乱、支付状态错误、页面半死不活。
第二,Crash 报告不能只存堆栈。一个能定位问题的报告,至少要有:
| 信息 | 作用 |
|---|---|
| 异常类型和堆栈 | 判断崩溃点 |
| 线程名 | 区分主线程、后台线程、线程池 |
| App 版本和构建号 | 定位引入版本 |
| Android 版本和设备型号 | 判断系统或机型相关 |
| 页面路径和业务动作 | 复现路径 |
| 用户状态和灰度配置 | 判断是否只影响某类用户 |
| 最近日志和关键埋点 | 还原崩溃前发生了什么 |
| 混淆映射版本 | 反混淆堆栈 |
#mermaid-svg-HGw8wsYad8RNgmSp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HGw8wsYad8RNgmSp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HGw8wsYad8RNgmSp .error-icon{fill:#552222;}#mermaid-svg-HGw8wsYad8RNgmSp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HGw8wsYad8RNgmSp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HGw8wsYad8RNgmSp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HGw8wsYad8RNgmSp .marker.cross{stroke:#333333;}#mermaid-svg-HGw8wsYad8RNgmSp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HGw8wsYad8RNgmSp p{margin:0;}#mermaid-svg-HGw8wsYad8RNgmSp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HGw8wsYad8RNgmSp .cluster-label text{fill:#333;}#mermaid-svg-HGw8wsYad8RNgmSp .cluster-label span{color:#333;}#mermaid-svg-HGw8wsYad8RNgmSp .cluster-label span p{background-color:transparent;}#mermaid-svg-HGw8wsYad8RNgmSp .label text,#mermaid-svg-HGw8wsYad8RNgmSp span{fill:#333;color:#333;}#mermaid-svg-HGw8wsYad8RNgmSp .node rect,#mermaid-svg-HGw8wsYad8RNgmSp .node circle,#mermaid-svg-HGw8wsYad8RNgmSp .node ellipse,#mermaid-svg-HGw8wsYad8RNgmSp .node polygon,#mermaid-svg-HGw8wsYad8RNgmSp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HGw8wsYad8RNgmSp .rough-node .label text,#mermaid-svg-HGw8wsYad8RNgmSp .node .label text,#mermaid-svg-HGw8wsYad8RNgmSp .image-shape .label,#mermaid-svg-HGw8wsYad8RNgmSp .icon-shape .label{text-anchor:middle;}#mermaid-svg-HGw8wsYad8RNgmSp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HGw8wsYad8RNgmSp .rough-node .label,#mermaid-svg-HGw8wsYad8RNgmSp .node .label,#mermaid-svg-HGw8wsYad8RNgmSp .image-shape .label,#mermaid-svg-HGw8wsYad8RNgmSp .icon-shape .label{text-align:center;}#mermaid-svg-HGw8wsYad8RNgmSp .node.clickable{cursor:pointer;}#mermaid-svg-HGw8wsYad8RNgmSp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HGw8wsYad8RNgmSp .arrowheadPath{fill:#333333;}#mermaid-svg-HGw8wsYad8RNgmSp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HGw8wsYad8RNgmSp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HGw8wsYad8RNgmSp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HGw8wsYad8RNgmSp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HGw8wsYad8RNgmSp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HGw8wsYad8RNgmSp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HGw8wsYad8RNgmSp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HGw8wsYad8RNgmSp .cluster text{fill:#333;}#mermaid-svg-HGw8wsYad8RNgmSp .cluster span{color:#333;}#mermaid-svg-HGw8wsYad8RNgmSp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HGw8wsYad8RNgmSp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HGw8wsYad8RNgmSp rect.text{fill:none;stroke-width:0;}#mermaid-svg-HGw8wsYad8RNgmSp .icon-shape,#mermaid-svg-HGw8wsYad8RNgmSp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HGw8wsYad8RNgmSp .icon-shape p,#mermaid-svg-HGw8wsYad8RNgmSp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HGw8wsYad8RNgmSp .icon-shape .label rect,#mermaid-svg-HGw8wsYad8RNgmSp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HGw8wsYad8RNgmSp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HGw8wsYad8RNgmSp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HGw8wsYad8RNgmSp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未捕获异常
UncaughtExceptionHandler
采集 Java 堆栈
采集设备和业务现场
写入本地文件
下次启动或当前进程尝试上传
稳定性平台聚合
分配 owner 修复
真正的优化不是给所有地方套 try-catch,而是按错误类型处理:
| 问题 | 更好的处理 |
|---|---|
| 空指针 | 数据模型明确可空边界,入口做校验 |
| 数组越界 | UI 下标和数据源长度绑定,异步刷新时防过期 |
| 非法状态 | 状态机收敛,避免任意状态调用任意方法 |
| JSON 解析失败 | 兼容字段缺失、类型错误和服务端灰度 |
| Activity 已销毁回调 | 生命周期感知取消,回调前检查状态 |
| 三方 SDK 异常 | 包装边界和降级策略,不让 SDK 拖垮主流程 |
一个小意见:核心链路不要过度依赖"大兜底"。兜底可以保护用户,但兜底太多会让问题沉到水下。真正要做的是让错误可见、可归因、可修复。
3. Native Crash:不是 Java 堆栈能救的
Native Crash 常见于音视频、图片处理、地图、WebView 扩展、游戏引擎、加密库、算法库等场景。它不一定会给你熟悉的 Java 异常,而是以信号形式出现,比如 SIGSEGV、SIGABRT。
Native Crash 的关键证据通常是 tombstone 和 native 堆栈。
#mermaid-svg-AHdwQGT6M4s7TyFP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AHdwQGT6M4s7TyFP .error-icon{fill:#552222;}#mermaid-svg-AHdwQGT6M4s7TyFP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AHdwQGT6M4s7TyFP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AHdwQGT6M4s7TyFP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AHdwQGT6M4s7TyFP .marker.cross{stroke:#333333;}#mermaid-svg-AHdwQGT6M4s7TyFP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AHdwQGT6M4s7TyFP p{margin:0;}#mermaid-svg-AHdwQGT6M4s7TyFP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP .cluster-label text{fill:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP .cluster-label span{color:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP .cluster-label span p{background-color:transparent;}#mermaid-svg-AHdwQGT6M4s7TyFP .label text,#mermaid-svg-AHdwQGT6M4s7TyFP span{fill:#333;color:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP .node rect,#mermaid-svg-AHdwQGT6M4s7TyFP .node circle,#mermaid-svg-AHdwQGT6M4s7TyFP .node ellipse,#mermaid-svg-AHdwQGT6M4s7TyFP .node polygon,#mermaid-svg-AHdwQGT6M4s7TyFP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AHdwQGT6M4s7TyFP .rough-node .label text,#mermaid-svg-AHdwQGT6M4s7TyFP .node .label text,#mermaid-svg-AHdwQGT6M4s7TyFP .image-shape .label,#mermaid-svg-AHdwQGT6M4s7TyFP .icon-shape .label{text-anchor:middle;}#mermaid-svg-AHdwQGT6M4s7TyFP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AHdwQGT6M4s7TyFP .rough-node .label,#mermaid-svg-AHdwQGT6M4s7TyFP .node .label,#mermaid-svg-AHdwQGT6M4s7TyFP .image-shape .label,#mermaid-svg-AHdwQGT6M4s7TyFP .icon-shape .label{text-align:center;}#mermaid-svg-AHdwQGT6M4s7TyFP .node.clickable{cursor:pointer;}#mermaid-svg-AHdwQGT6M4s7TyFP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AHdwQGT6M4s7TyFP .arrowheadPath{fill:#333333;}#mermaid-svg-AHdwQGT6M4s7TyFP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AHdwQGT6M4s7TyFP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AHdwQGT6M4s7TyFP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHdwQGT6M4s7TyFP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AHdwQGT6M4s7TyFP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHdwQGT6M4s7TyFP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AHdwQGT6M4s7TyFP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AHdwQGT6M4s7TyFP .cluster text{fill:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP .cluster span{color:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AHdwQGT6M4s7TyFP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AHdwQGT6M4s7TyFP rect.text{fill:none;stroke-width:0;}#mermaid-svg-AHdwQGT6M4s7TyFP .icon-shape,#mermaid-svg-AHdwQGT6M4s7TyFP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AHdwQGT6M4s7TyFP .icon-shape p,#mermaid-svg-AHdwQGT6M4s7TyFP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AHdwQGT6M4s7TyFP .icon-shape .label rect,#mermaid-svg-AHdwQGT6M4s7TyFP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AHdwQGT6M4s7TyFP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AHdwQGT6M4s7TyFP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AHdwQGT6M4s7TyFP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Native 代码异常
触发信号
系统生成 tombstone
用符号表还原堆栈
定位 so 和函数
修复越界 空指针 线程问题
Native Crash 排查里最容易踩两个坑:
- 没有保存符号表。线上堆栈只有地址,看起来像乱码。
- 只看 Java 调用点。真正崩溃可能在 native 内部线程,Java 层只是入口。
工程上建议:
| 做法 | 价值 |
|---|---|
| 每个版本保存符号表 | 线上 native 堆栈能还原函数名和行号 |
| 区分 ABI | arm64-v8a、armeabi-v7a 堆栈不能混用 |
| 记录 so 版本 | 防止热更新或动态库版本不一致 |
| 收集 tombstone | Android 12 以后可用 ApplicationExitInfo.getTraceInputStream() 读取相关 trace |
| 对 native 入口做参数校验 | 少把脏数据传进 C/C++ |
| 对第三方 so 建立灰度和开关 | 出问题时能快速止血 |
Native Crash 的修复成本通常比 Java Crash 高,所以版本发布前更要重视灰度和符号管理。没有符号表,线上定位会很痛。
4. ANR:用户说"卡死了",系统说"你没响应"
ANR 是 Application Not Responding。它不是异常崩溃,而是系统发现你的应用在规定时间内没有响应关键事件。
官方文档列出的常见 ANR 类型包括:
| ANR 类型 | 大白话 | 常见根因 |
|---|---|---|
| Input dispatch timed out | 用户点击或触摸后,主线程没及时处理 | 主线程长任务、锁等待、Binder 阻塞 |
| Service timeout | Service 生命周期或前台服务相关操作超时 | onCreate() / onStartCommand() 太重 |
| Broadcast timeout | 广播接收处理超时 | onReceive() 做 I/O 或同步等待 |
| ContentProvider timeout | Provider 发布或访问超时 | Provider 初始化过重、启动期 I/O |
| No focused window | 启动后没有及时创建可交互窗口 | 启动链路阻塞、首屏创建卡住 |
ANR 的核心问题通常是:主线程没有机会处理系统期待它处理的消息。
LockOrIO MainThread AndroidSystem User LockOrIO MainThread AndroidSystem User #mermaid-svg-KBnGXtGFzgT82Eqg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KBnGXtGFzgT82Eqg .error-icon{fill:#552222;}#mermaid-svg-KBnGXtGFzgT82Eqg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KBnGXtGFzgT82Eqg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KBnGXtGFzgT82Eqg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KBnGXtGFzgT82Eqg .marker.cross{stroke:#333333;}#mermaid-svg-KBnGXtGFzgT82Eqg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KBnGXtGFzgT82Eqg p{margin:0;}#mermaid-svg-KBnGXtGFzgT82Eqg .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KBnGXtGFzgT82Eqg text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KBnGXtGFzgT82Eqg .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-KBnGXtGFzgT82Eqg .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-KBnGXtGFzgT82Eqg #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-KBnGXtGFzgT82Eqg .sequenceNumber{fill:white;}#mermaid-svg-KBnGXtGFzgT82Eqg #sequencenumber{fill:#333;}#mermaid-svg-KBnGXtGFzgT82Eqg #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-KBnGXtGFzgT82Eqg .messageText{fill:#333;stroke:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KBnGXtGFzgT82Eqg .labelText,#mermaid-svg-KBnGXtGFzgT82Eqg .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .loopText,#mermaid-svg-KBnGXtGFzgT82Eqg .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KBnGXtGFzgT82Eqg .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-KBnGXtGFzgT82Eqg .noteText,#mermaid-svg-KBnGXtGFzgT82Eqg .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-KBnGXtGFzgT82Eqg .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KBnGXtGFzgT82Eqg .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KBnGXtGFzgT82Eqg .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KBnGXtGFzgT82Eqg .actorPopupMenu{position:absolute;}#mermaid-svg-KBnGXtGFzgT82Eqg .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-KBnGXtGFzgT82Eqg .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KBnGXtGFzgT82Eqg .actor-man circle,#mermaid-svg-KBnGXtGFzgT82Eqg line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-KBnGXtGFzgT82Eqg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} click or input 1 dispatch input event 2 wait lock or blocking IO 3 timeout check 4 ANR reported 5
ANR 排查不能只问"哪里卡了",要看主线程当时在等什么:
| 主线程栈表现 | 可能原因 |
|---|---|
Thread.sleep |
直接睡主线程 |
Object.wait / LockSupport.park |
等锁、等条件、等异步结果 |
BinderProxy.transact |
等系统服务或跨进程返回 |
| 文件读写 | 主线程 I/O |
| SQLite 操作 | 主线程数据库 |
| 图片解码 | 主线程 CPU 或内存压力 |
MessageQueue.nativePollOnce |
不一定是问题,要结合是否主线程空闲和其他线程状态 |
这里有个很常见的误会:看到主线程在 nativePollOnce,就以为没有卡。其实不一定。ANR trace 采样有时间差,如果抓栈时主线程已经恢复空闲,栈上可能就是 nativePollOnce。所以 ANR 要结合时间线、日志、Perfetto、其他线程锁持有者一起看。
5. ANR 优化:别让主线程"等答案"
ANR 的优化原则其实很朴素:主线程只做 UI 和很短的调度,不做重活,也不等重活。
5.1 不要在主线程做 I/O
错误示范:
kotlin
fun onCreate() {
val config = File(filesDir, "config.json").readText()
render(config)
}
更稳的写法:
kotlin
lifecycleScope.launch {
val config = withContext(Dispatchers.IO) {
File(filesDir, "config.json").readText()
}
render(config)
}
如果配置是首屏必须的,优先考虑更小的本地缓存、启动前置预热、异步骨架屏,而不是让主线程硬等。
5.2 不要在主线程等锁
比较隐蔽的 ANR 通常来自锁:
kotlin
private val lock = Any()
fun updateOnMain() {
synchronized(lock) {
renderState()
}
}
fun workInBackground() {
synchronized(lock) {
doSlowIO()
}
}
后台线程拿着锁做慢 I/O,主线程进来等锁,用户一点击就可能 ANR。修复方向是缩小锁范围,不要在锁里做 I/O,不要让主线程等待后台长任务。
5.3 Broadcast 和 Service 要快进快出
广播的 onReceive() 不适合做长任务。Service 的生命周期方法也不适合塞重初始化。更推荐把耗时任务丢给 WorkManager、前台服务的后台线程、协程或专用线程池。
kotlin
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
WorkManager.getInstance(context)
.enqueue(buildSyncWork())
}
}
这段代码只表达方向:广播里触发调度,真正耗时的工作交给后台执行。
6. OOM:不只是 Java 堆爆了
OOM 也要分情况。
| 类型 | 表现 | 常见原因 |
|---|---|---|
| Java heap OOM | 抛 OutOfMemoryError |
大对象、集合失控、泄漏 |
| Native OOM | native 分配失败或崩溃 | Bitmap、音视频、so 内存 |
| Graphics 内存压力 | 渲染或图片相关异常 | 大图、纹理、硬件位图 |
| LMK 杀进程 | 进程突然没了,没有 Java 堆栈 | 系统低内存回收 |
OOM 优化不要只盯 Java heap。Android 上 Bitmap、native buffer、graphics 内存、WebView、播放器、相机都可能把进程推到危险边缘。
常见治理点:
| 问题 | 优化方向 |
|---|---|
| 大图直接加载 | 按 View 尺寸解码,限制原图进入内存 |
| Bitmap 缓存无上限 | LRU、内存等级感知、页面退出清理 |
| Activity 泄漏 | LeakCanary、生命周期解绑、弱化回调持有 |
| 列表无限数据 | 分页、裁剪、Diff、避免全量常驻 |
| WebView 内存高 | 独立进程、按需创建、页面销毁策略 |
| native 内存泄漏 | heapprofd、meminfo、native 分配点排查 |
#mermaid-svg-qHshWedIINxCrAPL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qHshWedIINxCrAPL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qHshWedIINxCrAPL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qHshWedIINxCrAPL .error-icon{fill:#552222;}#mermaid-svg-qHshWedIINxCrAPL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qHshWedIINxCrAPL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qHshWedIINxCrAPL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qHshWedIINxCrAPL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qHshWedIINxCrAPL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qHshWedIINxCrAPL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qHshWedIINxCrAPL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qHshWedIINxCrAPL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qHshWedIINxCrAPL .marker.cross{stroke:#333333;}#mermaid-svg-qHshWedIINxCrAPL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qHshWedIINxCrAPL p{margin:0;}#mermaid-svg-qHshWedIINxCrAPL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qHshWedIINxCrAPL .cluster-label text{fill:#333;}#mermaid-svg-qHshWedIINxCrAPL .cluster-label span{color:#333;}#mermaid-svg-qHshWedIINxCrAPL .cluster-label span p{background-color:transparent;}#mermaid-svg-qHshWedIINxCrAPL .label text,#mermaid-svg-qHshWedIINxCrAPL span{fill:#333;color:#333;}#mermaid-svg-qHshWedIINxCrAPL .node rect,#mermaid-svg-qHshWedIINxCrAPL .node circle,#mermaid-svg-qHshWedIINxCrAPL .node ellipse,#mermaid-svg-qHshWedIINxCrAPL .node polygon,#mermaid-svg-qHshWedIINxCrAPL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qHshWedIINxCrAPL .rough-node .label text,#mermaid-svg-qHshWedIINxCrAPL .node .label text,#mermaid-svg-qHshWedIINxCrAPL .image-shape .label,#mermaid-svg-qHshWedIINxCrAPL .icon-shape .label{text-anchor:middle;}#mermaid-svg-qHshWedIINxCrAPL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qHshWedIINxCrAPL .rough-node .label,#mermaid-svg-qHshWedIINxCrAPL .node .label,#mermaid-svg-qHshWedIINxCrAPL .image-shape .label,#mermaid-svg-qHshWedIINxCrAPL .icon-shape .label{text-align:center;}#mermaid-svg-qHshWedIINxCrAPL .node.clickable{cursor:pointer;}#mermaid-svg-qHshWedIINxCrAPL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qHshWedIINxCrAPL .arrowheadPath{fill:#333333;}#mermaid-svg-qHshWedIINxCrAPL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qHshWedIINxCrAPL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qHshWedIINxCrAPL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qHshWedIINxCrAPL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qHshWedIINxCrAPL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qHshWedIINxCrAPL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qHshWedIINxCrAPL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qHshWedIINxCrAPL .cluster text{fill:#333;}#mermaid-svg-qHshWedIINxCrAPL .cluster span{color:#333;}#mermaid-svg-qHshWedIINxCrAPL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qHshWedIINxCrAPL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qHshWedIINxCrAPL rect.text{fill:none;stroke-width:0;}#mermaid-svg-qHshWedIINxCrAPL .icon-shape,#mermaid-svg-qHshWedIINxCrAPL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qHshWedIINxCrAPL .icon-shape p,#mermaid-svg-qHshWedIINxCrAPL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qHshWedIINxCrAPL .icon-shape .label rect,#mermaid-svg-qHshWedIINxCrAPL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qHshWedIINxCrAPL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qHshWedIINxCrAPL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qHshWedIINxCrAPL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内存压力上升
Java heap
Native heap
Graphics
LMK 杀进程
对象泄漏
大对象
Native 分配
图片和纹理
ApplicationExitInfo
如果线上看到"进程消失但没有 Crash 堆栈",不要急着说平台漏采。它可能是低内存杀进程、用户手动杀、系统限制、后台限制。Android 11 以后可以通过 ApplicationExitInfo 查询历史退出原因,这对分析 ANR、低内存、Native Crash 等情况很有用。
7. 线上采集:报告要能还原现场
一个稳定性平台,最重要的不是"能收到异常",而是能不能帮助你还原现场。
我会把采集分成四层:
#mermaid-svg-zmWTvnsYhLzTbS5M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zmWTvnsYhLzTbS5M .error-icon{fill:#552222;}#mermaid-svg-zmWTvnsYhLzTbS5M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zmWTvnsYhLzTbS5M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zmWTvnsYhLzTbS5M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zmWTvnsYhLzTbS5M .marker.cross{stroke:#333333;}#mermaid-svg-zmWTvnsYhLzTbS5M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zmWTvnsYhLzTbS5M p{margin:0;}#mermaid-svg-zmWTvnsYhLzTbS5M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M .cluster-label text{fill:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M .cluster-label span{color:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M .cluster-label span p{background-color:transparent;}#mermaid-svg-zmWTvnsYhLzTbS5M .label text,#mermaid-svg-zmWTvnsYhLzTbS5M span{fill:#333;color:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M .node rect,#mermaid-svg-zmWTvnsYhLzTbS5M .node circle,#mermaid-svg-zmWTvnsYhLzTbS5M .node ellipse,#mermaid-svg-zmWTvnsYhLzTbS5M .node polygon,#mermaid-svg-zmWTvnsYhLzTbS5M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zmWTvnsYhLzTbS5M .rough-node .label text,#mermaid-svg-zmWTvnsYhLzTbS5M .node .label text,#mermaid-svg-zmWTvnsYhLzTbS5M .image-shape .label,#mermaid-svg-zmWTvnsYhLzTbS5M .icon-shape .label{text-anchor:middle;}#mermaid-svg-zmWTvnsYhLzTbS5M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zmWTvnsYhLzTbS5M .rough-node .label,#mermaid-svg-zmWTvnsYhLzTbS5M .node .label,#mermaid-svg-zmWTvnsYhLzTbS5M .image-shape .label,#mermaid-svg-zmWTvnsYhLzTbS5M .icon-shape .label{text-align:center;}#mermaid-svg-zmWTvnsYhLzTbS5M .node.clickable{cursor:pointer;}#mermaid-svg-zmWTvnsYhLzTbS5M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zmWTvnsYhLzTbS5M .arrowheadPath{fill:#333333;}#mermaid-svg-zmWTvnsYhLzTbS5M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zmWTvnsYhLzTbS5M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zmWTvnsYhLzTbS5M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zmWTvnsYhLzTbS5M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zmWTvnsYhLzTbS5M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zmWTvnsYhLzTbS5M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zmWTvnsYhLzTbS5M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zmWTvnsYhLzTbS5M .cluster text{fill:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M .cluster span{color:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zmWTvnsYhLzTbS5M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zmWTvnsYhLzTbS5M rect.text{fill:none;stroke-width:0;}#mermaid-svg-zmWTvnsYhLzTbS5M .icon-shape,#mermaid-svg-zmWTvnsYhLzTbS5M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zmWTvnsYhLzTbS5M .icon-shape p,#mermaid-svg-zmWTvnsYhLzTbS5M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zmWTvnsYhLzTbS5M .icon-shape .label rect,#mermaid-svg-zmWTvnsYhLzTbS5M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zmWTvnsYhLzTbS5M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zmWTvnsYhLzTbS5M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zmWTvnsYhLzTbS5M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 异常或卡死事件
运行时信息
设备和系统信息
业务现场
时间线
稳定性报告
聚合归因
修复和验证
具体字段可以按这个表设计:
| 维度 | 建议采集 |
|---|---|
| 运行时 | 线程堆栈、异常类型、ANR trace、native tombstone、内存快照摘要 |
| 设备 | 系统版本、机型、ABI、内存等级、屏幕、厂商 ROM |
| App | 版本、构建号、渠道、灰度组、混淆映射版本 |
| 页面 | 当前 Activity、Fragment、路由、Tab、前后台状态 |
| 用户动作 | 最近点击、接口请求、页面跳转、关键埋点 |
| 资源 | 内存占用、线程数、文件句柄、磁盘空间 |
| 网络 | 网络类型、请求错误、超时、服务端状态码 |
| 发布 | 是否热修、是否动态配置、是否实验开关命中 |
采集越多越好吗?也不是。要注意隐私、体积和性能成本。稳定性日志最好记录"定位必要的信息",不要把用户敏感数据和完整业务内容无脑上传。
8. 治理闭环:从发现到止血,再到防复发
稳定性问题要按事故流程处理,而不是靠个人英雄主义。
#mermaid-svg-ChtCr43Lb0JFscnO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ChtCr43Lb0JFscnO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ChtCr43Lb0JFscnO .error-icon{fill:#552222;}#mermaid-svg-ChtCr43Lb0JFscnO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ChtCr43Lb0JFscnO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ChtCr43Lb0JFscnO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ChtCr43Lb0JFscnO .marker.cross{stroke:#333333;}#mermaid-svg-ChtCr43Lb0JFscnO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ChtCr43Lb0JFscnO p{margin:0;}#mermaid-svg-ChtCr43Lb0JFscnO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ChtCr43Lb0JFscnO .cluster-label text{fill:#333;}#mermaid-svg-ChtCr43Lb0JFscnO .cluster-label span{color:#333;}#mermaid-svg-ChtCr43Lb0JFscnO .cluster-label span p{background-color:transparent;}#mermaid-svg-ChtCr43Lb0JFscnO .label text,#mermaid-svg-ChtCr43Lb0JFscnO span{fill:#333;color:#333;}#mermaid-svg-ChtCr43Lb0JFscnO .node rect,#mermaid-svg-ChtCr43Lb0JFscnO .node circle,#mermaid-svg-ChtCr43Lb0JFscnO .node ellipse,#mermaid-svg-ChtCr43Lb0JFscnO .node polygon,#mermaid-svg-ChtCr43Lb0JFscnO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ChtCr43Lb0JFscnO .rough-node .label text,#mermaid-svg-ChtCr43Lb0JFscnO .node .label text,#mermaid-svg-ChtCr43Lb0JFscnO .image-shape .label,#mermaid-svg-ChtCr43Lb0JFscnO .icon-shape .label{text-anchor:middle;}#mermaid-svg-ChtCr43Lb0JFscnO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ChtCr43Lb0JFscnO .rough-node .label,#mermaid-svg-ChtCr43Lb0JFscnO .node .label,#mermaid-svg-ChtCr43Lb0JFscnO .image-shape .label,#mermaid-svg-ChtCr43Lb0JFscnO .icon-shape .label{text-align:center;}#mermaid-svg-ChtCr43Lb0JFscnO .node.clickable{cursor:pointer;}#mermaid-svg-ChtCr43Lb0JFscnO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ChtCr43Lb0JFscnO .arrowheadPath{fill:#333333;}#mermaid-svg-ChtCr43Lb0JFscnO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ChtCr43Lb0JFscnO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ChtCr43Lb0JFscnO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ChtCr43Lb0JFscnO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ChtCr43Lb0JFscnO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ChtCr43Lb0JFscnO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ChtCr43Lb0JFscnO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ChtCr43Lb0JFscnO .cluster text{fill:#333;}#mermaid-svg-ChtCr43Lb0JFscnO .cluster span{color:#333;}#mermaid-svg-ChtCr43Lb0JFscnO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ChtCr43Lb0JFscnO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ChtCr43Lb0JFscnO rect.text{fill:none;stroke-width:0;}#mermaid-svg-ChtCr43Lb0JFscnO .icon-shape,#mermaid-svg-ChtCr43Lb0JFscnO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ChtCr43Lb0JFscnO .icon-shape p,#mermaid-svg-ChtCr43Lb0JFscnO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ChtCr43Lb0JFscnO .icon-shape .label rect,#mermaid-svg-ChtCr43Lb0JFscnO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ChtCr43Lb0JFscnO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ChtCr43Lb0JFscnO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ChtCr43Lb0JFscnO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发现指标异常
分级和归因
止血
修复根因
灰度验证
扩大发布
复盘和防回归
门禁和监控
每一步都有不同重点:
| 阶段 | 目标 | 常见动作 |
|---|---|---|
| 发现 | 知道出问题 | 告警、看板、版本对比 |
| 分级 | 判断严重程度 | 影响用户数、是否启动路径、是否支付路径 |
| 止血 | 降低损伤 | 关闭开关、回滚配置、暂停灰度、下架版本 |
| 修复 | 解决根因 | 代码修复、兼容、补测试 |
| 验证 | 确认有效 | 灰度、复现用例、指标回落 |
| 复盘 | 防止再犯 | 增加门禁、监控、规范、owner |
止血能力特别重要。线上大事故时,最怕只有"发新版修复"一条路。稳定性好的 App 通常会准备:
| 能力 | 用处 |
|---|---|
| 配置开关 | 快速关闭高风险功能 |
| 灰度发布 | 限制影响范围 |
| 服务端降级 | 接口异常时给默认值或兜底页 |
| 客户端降级 | 页面模块加载失败时不拖垮整页 |
| 版本回滚 | 发布平台允许时快速回退 |
| 热修能力 | 谨慎用于明确且低风险的问题 |
但别滥用热修。热修适合小范围、明确原因、风险可控的问题。线程模型、内存生命周期、复杂状态机这类问题,热修有时会把坑补成另一个坑。
9. 预防比修复更便宜
稳定性优化不该只发生在线上事故后。开发阶段就能做很多事。
9.1 编码层防线
| 场景 | 建议 |
|---|---|
| 可空数据 | Kotlin 可空类型、入口校验、默认值 |
| 状态流转 | 用明确状态机,不让任意状态调用任意方法 |
| 生命周期 | 协程作用域、取消回调、页面销毁不更新 UI |
| 线程 | 主线程只做 UI,重活切后台 |
| 集合访问 | 防止异步刷新后下标过期 |
| 外部输入 | 服务端字段、Intent 参数、DeepLink 参数都要校验 |
9.2 工程层防线
| 防线 | 作用 |
|---|---|
| Lint 和静态扫描 | 提前发现主线程 I/O、危险 API、空指针风险 |
| 单元测试 | 覆盖状态机、边界输入、解析兼容 |
| UI 自动化 | 覆盖核心路径和启动路径 |
| Monkey / 稳定性测试 | 找随机交互下的崩溃和 ANR |
| 内存专项 | 找泄漏、图片内存、缓存失控 |
| 性能专项 | 找卡顿、启动慢、主线程长任务 |
| 发布门禁 | Crash/ANR 指标异常时阻止扩大灰度 |
9.3 依赖治理
很多稳定性问题来自 SDK:
- 广告 SDK 在启动期初始化过重。
- 地图 SDK 在低端机 native crash。
- 推送 SDK 多进程初始化不区分进程。
- 图片库配置不当导致 OOM。
- WebView 容器持有 Activity 泄漏。
三方 SDK 要有 owner,有版本记录,有灰度,有开关。不要让它们在 Application.onCreate() 里随便跑,更不要让 SDK 异常直接拖垮主流程。
10. ANR 与 Crash 的排查顺序
线上来了一个稳定性问题,可以按这个顺序走:
#mermaid-svg-Qo1idEAPlLXEajoC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qo1idEAPlLXEajoC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qo1idEAPlLXEajoC .error-icon{fill:#552222;}#mermaid-svg-Qo1idEAPlLXEajoC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qo1idEAPlLXEajoC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qo1idEAPlLXEajoC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qo1idEAPlLXEajoC .marker.cross{stroke:#333333;}#mermaid-svg-Qo1idEAPlLXEajoC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qo1idEAPlLXEajoC p{margin:0;}#mermaid-svg-Qo1idEAPlLXEajoC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Qo1idEAPlLXEajoC .cluster-label text{fill:#333;}#mermaid-svg-Qo1idEAPlLXEajoC .cluster-label span{color:#333;}#mermaid-svg-Qo1idEAPlLXEajoC .cluster-label span p{background-color:transparent;}#mermaid-svg-Qo1idEAPlLXEajoC .label text,#mermaid-svg-Qo1idEAPlLXEajoC span{fill:#333;color:#333;}#mermaid-svg-Qo1idEAPlLXEajoC .node rect,#mermaid-svg-Qo1idEAPlLXEajoC .node circle,#mermaid-svg-Qo1idEAPlLXEajoC .node ellipse,#mermaid-svg-Qo1idEAPlLXEajoC .node polygon,#mermaid-svg-Qo1idEAPlLXEajoC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Qo1idEAPlLXEajoC .rough-node .label text,#mermaid-svg-Qo1idEAPlLXEajoC .node .label text,#mermaid-svg-Qo1idEAPlLXEajoC .image-shape .label,#mermaid-svg-Qo1idEAPlLXEajoC .icon-shape .label{text-anchor:middle;}#mermaid-svg-Qo1idEAPlLXEajoC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Qo1idEAPlLXEajoC .rough-node .label,#mermaid-svg-Qo1idEAPlLXEajoC .node .label,#mermaid-svg-Qo1idEAPlLXEajoC .image-shape .label,#mermaid-svg-Qo1idEAPlLXEajoC .icon-shape .label{text-align:center;}#mermaid-svg-Qo1idEAPlLXEajoC .node.clickable{cursor:pointer;}#mermaid-svg-Qo1idEAPlLXEajoC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Qo1idEAPlLXEajoC .arrowheadPath{fill:#333333;}#mermaid-svg-Qo1idEAPlLXEajoC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Qo1idEAPlLXEajoC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Qo1idEAPlLXEajoC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qo1idEAPlLXEajoC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Qo1idEAPlLXEajoC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qo1idEAPlLXEajoC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Qo1idEAPlLXEajoC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Qo1idEAPlLXEajoC .cluster text{fill:#333;}#mermaid-svg-Qo1idEAPlLXEajoC .cluster span{color:#333;}#mermaid-svg-Qo1idEAPlLXEajoC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Qo1idEAPlLXEajoC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Qo1idEAPlLXEajoC rect.text{fill:none;stroke-width:0;}#mermaid-svg-Qo1idEAPlLXEajoC .icon-shape,#mermaid-svg-Qo1idEAPlLXEajoC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qo1idEAPlLXEajoC .icon-shape p,#mermaid-svg-Qo1idEAPlLXEajoC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Qo1idEAPlLXEajoC .icon-shape .label rect,#mermaid-svg-Qo1idEAPlLXEajoC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qo1idEAPlLXEajoC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Qo1idEAPlLXEajoC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Qo1idEAPlLXEajoC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Java Crash
Native Crash
ANR
OOM
稳定性问题
先分类
看异常类型和主堆栈
看 signal 和 tombstone
看主线程栈和等待链
看内存曲线和对象持有
看影响版本和用户
构造复现场景
修复和灰度验证
更细一点:
| 问题 | 第一眼看什么 | 第二步看什么 |
|---|---|---|
| Java Crash | 异常类型、顶部业务堆栈 | 页面、参数、版本差异 |
| Native Crash | signal、崩溃线程、so 名称 | 符号化、ABI、设备集中度 |
| ANR | 主线程栈 | 锁持有者、Binder 对端、I/O、Perfetto |
| OOM | OOM 类型、内存区域 | 图片、缓存、泄漏、native 分配 |
| 启动失败 | 是否 Provider / Application / 首 Activity | 启动链路埋点和灰度配置 |
一个小技巧:不要只看 Top 1。Top 1 可能是兜底异常,比如 IllegalStateException,真正根因藏在更早的业务日志里。稳定性平台最好能看"崩溃前 30 秒发生了什么"。
11. 面试怎么讲崩溃优化和稳定性
面试里如果问"你做过崩溃优化吗",不要只说"接了 Crash 平台,修了空指针"。可以这样讲:
text
我会先把稳定性问题分类,不会只看 Crash。
Crash 里又分 Java Crash 和 Native Crash;ANR 是主线程或关键组件超时没有响应;OOM 还要区分 Java heap、native 内存、graphics 内存和低内存杀进程。
采集上,Java Crash 可以通过 UncaughtExceptionHandler 记录异常、线程、设备、版本、页面和业务现场;Native Crash 要保留 tombstone 和符号表;ANR 要看 traces、主线程堆栈、锁等待、Binder 和 I/O;Android 11 以后还可以通过 ApplicationExitInfo 辅助分析历史退出原因。
优化上,我不会靠全局 try-catch 硬吞异常,而是按根因治理:空指针和状态异常靠数据校验和状态机;ANR 靠主线程治理、I/O 下沉、锁范围收敛;OOM 靠图片内存、缓存、泄漏和 native 内存治理;线上再配合灰度、开关、降级、回滚和告警。
指标上,我会看 Crash 率、ANR 率、启动失败率、影响用户数、Top 问题占比和新版本新增问题。最终要形成发现、止血、修复、验证、防回归的闭环。
如果面试官继续问"ANR 怎么排查",可以接:
text
我会先看 ANR 类型,是 input、broadcast、service 还是 provider。
然后看主线程栈,判断是在执行耗时任务、等锁、Binder 调用、磁盘 I/O,还是采样时已经回到 nativePollOnce。
如果是锁等待,要找持锁线程;如果是 Binder,要看对端服务;如果是启动期 ANR,要重点查 ContentProvider、Application 和首屏初始化。
最后用 Perfetto 或线上 trace 还原时间线,而不是只凭一段主线程堆栈下结论。
这套回答的关键是:你把稳定性当系统工程,而不是把崩溃当零散 bug。
12. 小结
稳定性治理可以压成一句话:
text
先分类,再采集;先止血,再修根因;最后把经验变成门禁和监控。
Crash、ANR、OOM、Native Crash 的排查路径不同,但它们最后都会回到同一套工程能力:现场要采得到,问题要分得清,影响面要控得住,修复要验证,复发要被门禁拦住。
所以别把崩溃优化理解成"少崩几次"。真正的目标是让 App 在真实设备、真实网络、真实用户行为下,遇到异常也能尽量降级,出了问题能快速定位,发版之后能被指标守住。
13. 源码与参考入口
以下入口按 2026-10-09 查询和整理,用来辅助定位官方口径;具体实现请以项目目标 Android 版本、Google Play Console 当前规则和接入平台为准。
| 主题 | 链接 |
|---|---|
| Android Vitals 总览 | Monitor your app's technical quality with Android vitals |
| Crash bad behavior 阈值 | Crash rate |
| ANR bad behavior 阈值 | ANR rate |
| ANR 诊断与修复 | Diagnose and fix ANRs |
| ApplicationExitInfo | ApplicationExitInfo reference |
| Tombstone 和 native crash 信息 | ApplicationExitInfo API |
| Android 性能测量总览 | Overview of measuring app performance |