Crash 的产生原因与常见类型归纳总结
咱们直接切入正题。Crash 就是 App 突然“崩了”,用户看到闪退或者系统弹窗。说白了,是应用进程被操作系统强杀,或者因为未捕获的异常导致线程挂掉。我手上处理过的线上问题里,Crash 占了六成以上——大部分都能快速定位,但有些坑,只有踩过才懂。
Crash 的核心原因:两类源头
通常我会把 Crash 分成两类来排查:
- Java / Kotlin 异常 —— 未捕获的 Throwable 子类,比如空指针、数组越界。
- Native Crash —— C/C++ 层的内存问题,比如野指针、堆栈溢出、SIGSEGV 信号。
其实多数业务层的 Crash 都属于第一类,第二类常见于音视频、游戏、OpenGL 等场景。我在项目中遇到过两三次 Native Crash,堆栈全是问号,最后靠 addr2line 和 ndk-stack 才揪出问题——嗯,这里要特别注意,Native Crash 的定位工具链和 Java 层完全不同。
常见 Java / Kotlin Crash 类型
我列一个实战中高频出现的表格,你对照着看堆栈就能快速判断。
| 异常类型 | 典型原因 | 堆栈特征 | 我踩过的坑 |
|---|---|---|---|
NullPointerException |
对象未初始化、回调空引用 | 明确指向空值调用行 | Fragment 重建后 View 为 null |
IndexOutOfBoundsException |
列表索引越界、数组长度不足 | 提示 "length=… index=…" | 多线程同时修改 ArrayList |
ClassCastException |
强转不匹配类型 | 显示 expected vs actual class | Parcelable 对象序列化后类型混淆 |
IllegalStateException |
状态错误,如 Fragment 提交后操作 | "Can not perform this action after onSaveInstanceState" | 异步回调里提交 FragmentTransaction |
OutOfMemoryError |
堆内存耗尽、位图过大 | "Failed to allocate…" | 大图未释放导致的连续 OOM |
Native Crash 的典型场景
为什么要把 Native Crash 单独拎出来?因为它的堆栈不像 Java 那样好读。常见原因包括:
- 野指针 / 悬空指针 —— 释放后继续使用
- 堆栈溢出 —— 递归太深或局部变量过大
- ABI 不匹配 —— 加载了错误架构的 so 库
- 信号处理错误 —— SIGABRT、SIGSEGV
我印象很深的一个线上 Native Crash:某个低端机型上播放视频时必崩,最后用 minidump 分析发现是 ffmpeg 库的 libx264 编解码器用了非对齐内存访问。解决方式是把 malloc 换成 aligned_alloc。你看,Native 层面的坑往往和硬件底层相关。
Crash 定位的核心逻辑
不管哪种类型,定位套路其实差不多:
- 抓日志 —— 拿到 logcat 中的异常堆栈或 tombstone 文件
- 读关键词 —— 异常类名、消息、行号、so 名
- 复现场景 —— 操作路径、系统版本、机型
- 打日志或插桩 —— 缩小怀疑范围
- 修复验证 —— 改完后灰度测试
小技巧: 使用 Thread.setDefaultUncaughtExceptionHandler 可以捕获 Java 层未处理异常,但 Native Crash 需要 Google Breakpad 或 Crashpad 这样的库来生成 minidump。我建议所有重要 App 都集成本地崩溃收集,否则线上 Native Crash 几乎不可见。
Crash 与 ANR 的一个关键区别
很多新手会把 ANR 当成 Crash,其实两者不同。Crash 是进程异常终止;ANR 是进程还活着,但主线程卡死超过 5 秒。判断方法很简单——看日志里有没有 ANR in … 关键字。我曾经在一次线上事故里误把 ANR 堆栈当成 Crash 去修,浪费了两天。嗯,这里一定要先确认是哪种异常。
避坑指南: 我曾经接手过一个项目,每天收到几千条“系统 UI 崩溃”的报警,结果发现是某个第三方 SDK 在 Native 层发了一个空信号给 Android 系统服务。我用 dumpsys dropbox 把系统级 tombstone 拉下来才找到真凶。所以不要只盯着自己进程的日志,系统 dropbox 里也有崩溃记录。
一张图看懂 Crash 产生原因
下面是 Crash 原因的层级结构图,你可以用来快速梳理排查方向。
实战定位踩坑提醒
Crash 定位最怕的是——你只看到了表象。比如 NullPointerException,有时候是异步时序导致的,不是真的 null。我分享几个曾经遇到的坑:
- 「Crash 总是特定机型出现」 —— 可能是 ROM 的定制化问题,比如小米的 MIUI 对某些权限处理不同。我曾经遇到华为 EMUI 上
InputMethodManager的NullPointerException,原因是系统在 Activity 销毁后还持有引用。 - 「同一 Crash 重复上报」 —— 检查一下是不是 ANR 被错误归类为 Crash。系统
AnrInputStream的堆栈有时也会以AppNotRespondingDialog的形式出现。 - 「OOM 堆栈指向 SDK 内部」 —— 别急着怀疑 SDK 有 Bug,先看看是不是你们自己传了超大图片过去。我调过微信分享 SDK 的 OOM,最后发现是图片加载没有缩放到可用内存。
重要提醒: 一旦线上出现高频 Crash,第一时间做三件事:1. 看版本分布(是不是刚发的新版);2. 看机型分布(集中某个品牌);3. 看操作栈(是不是固定路径)。如果三者都指向一个模块,别犹豫,直接回滚旧版本,同时拉红线上修复。
总结 Crash 定位的五个关键动作
- 明确类型 —— Java 还是 Native?系统还是应用层?
- 提取上下文 —— 机型、系统版本、App 版本、堆栈全文
- 检查多线程 —— 很多 Crash 其实是对共享集合操作不加锁导致的
- 验证资源释放 —— Bitmap、Cursor、Socket 是否泄漏
- 看系统日志 —— 尤其是 lowMemoryKiller、watchdog、dropbox 的输出
最后一句话:Crash 不可怕,可怕的是看不懂堆栈。保持对堆栈的敏感度,多积累常见模式,你也能一眼看出病因。我习惯在工程里建一个「Crash 知识库」,每次遇到新类型就记录特征和修复方案——这是性价比最高的自我迭代方式。