Crash 基础概念
什么是 Crash
Crash,说白了就是程序突然“死”了。用户正用着手机呢,屏幕一卡,然后“嘣”一下回到桌面——这就是典型的 Crash。从技术上讲,Crash 是应用遇到了无法恢复的异常,操作系统被迫把进程杀掉。
我在项目中遇到过最夸张的一次,某个版本上线后 Crash 率飙升到 15%。查了半天,发现是一行代码里的野指针。你看,一个小小的指针没处理好,就能让整个产品崩掉。
常见的触发场景包括:
- 访问了已经释放的内存(野指针、悬空指针)
- 数组下标越界(比如只有 10 个元素,你去访问第 11 个)
- 多线程数据竞争(两个线程同时写同一个变量)
- 空指针调用(你调用了一个 null 对象的方法)
- 堆内存耗尽(OOM,Out of Memory)
Crash 的影响有多严重?
可能有人觉得“崩一下也没啥大不了”,重启就好了。但真实情况远没那么简单。
| 影响维度 | 具体表现 | 我的经验值 |
|---|---|---|
| 用户体验 | 用户流失、差评、卸载 | Crash 率每上升 1%,日活可能掉 2%~5% |
| 业务收入 | 支付中断、广告损失、订阅退费 | 我之前负责的电商 App,Crash 导致下单页崩溃,一晚上损失了十几万 GMV |
| 开发者口碑 | 应用商店评分下降,品牌受损 | 某次紧急发版修复后,用户评论“终于不闪退了” |
| 研发效率 | 需要投入大量人力排查和修复 | 最头疼的是“偶发 Crash”,埋点都拿不到堆栈 |
避坑指南: 我曾经因为忽略了一个只在 Android 12 上出现的 Crash,导致灰度发布时炸了一半的用户。从此我再也不敢只看 top crash 列表,那些低频但重复出现的异常也要重视。
Crash 分类概览
Crash 种类很多,但咱们做复现框架,必须先学会分类。只有分对了类,才能针对性设计复现策略。我个人习惯按根本原因分这几类:
- 内存类 Crash:野指针、空指针、内存泄漏、释放后使用(Use-After-Free)、缓冲区溢出。这类最头疼,因为崩溃点往往离真正出错的地方很远。
- 逻辑类 Crash:数组越界、类型转换错误、除零、死循环导致 ANR(应用无响应)。这类相对容易复现,加断言就能抓到。
- 并发类 Crash:线程安全(死锁、活锁)、数据竞争、ABA 问题、线程池耗尽。我遇到过最邪门的是在春运峰值 QPS 下才复现的并发 Bug,本地跑一万次都正常。
- 资源类 Crash:OOM(堆、栈、文件描述符耗尽)、数据库锁、网络超时连锁反应。这类在低端机型上特别多。
- 平台/环境类 Crash:系统 API 变更、第三方 SDK 兼容性、厂商定制 ROM 差异。说实话,这种问题只能靠线上监控和灰度备份。
有了分类,你就能一眼看出问题的“家族”。比如同样是 SIGSEGV,可能是野指针也可能是空指针——但处理思路完全不同。
个人习惯: 我每次拿到一个新 Crash 报告,第一件事不是看堆栈,而是先看它属于哪一类。然后脑子里自动匹配该类的复现套路。比如,内存类先 check 是否开启了 ASAN,并发类先看线程日志。
注意: 很多 Crash 表面上属于一类,根因可能跨类。比如 OOM 有时候是内存泄漏(内存类),有时候是图片加载太多(资源类),还有时候是线程栈溢出(逻辑/资源混合)。所以分类并不是的,但框架得先有个骨架。
从分类到复现 — 我们要做什么?
分类只是第一步。接下来我们要搭建复现框架,需要针对每一类设计不同的复现手段。举个例子:
- 内存类 Crash:用 AddressSanitizer 或 Valgrind 很容易捕捉,但线上环境不能开这些工具。所以我们得在代码层加 Hook 或者用 MMKV 模拟内存压力。
- 并发类 Crash:用线程安全检查工具(TSAN),或者通过增加线程数、降低 sleep 时间来触发竞争。
- 资源类 Crash:模拟低内存、低磁盘、慢网速环境,用手动触发系统 GC。
- 环境类 Crash:只能在真机集群上跑自动化 Monkey 测试,覆盖不同系统版本和厂商。
这章先把分类讲清楚。后面的章节,我会手把手带你搭一个通用的复现框架。你想想看,一旦有了这个框架,再棘手的 Crash 你也能快速复现出来,然后修复它就只是时间问题了。