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 定位的核心逻辑

不管哪种类型,定位套路其实差不多:

  1. 抓日志 —— 拿到 logcat 中的异常堆栈或 tombstone 文件
  2. 读关键词 —— 异常类名、消息、行号、so 名
  3. 复现场景 —— 操作路径、系统版本、机型
  4. 打日志或插桩 —— 缩小怀疑范围
  5. 修复验证 —— 改完后灰度测试

小技巧: 使用 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 产生原因分类 App Crash Java/Kotlin 异常 Native Crash 系统级异常 空指针 · 数组越界 类型转换 · 状态错误 OOM · 资源未找到 野指针 · 内存越界 堆栈溢出 · 信号 ABI 不匹配 · so 库 系统服务死锁 空指针系统回调 SELinux 拒绝 定位套路:抓日志 → 读关键词 → 复现场景 → 打日志/插桩 → 修复验证 * 系统级异常需要通过 dumpsys dropbox 或系统 log 获取

实战定位踩坑提醒

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 定位的五个关键动作

  1. 明确类型 —— Java 还是 Native?系统还是应用层?
  2. 提取上下文 —— 机型、系统版本、App 版本、堆栈全文
  3. 检查多线程 —— 很多 Crash 其实是对共享集合操作不加锁导致的
  4. 验证资源释放 —— Bitmap、Cursor、Socket 是否泄漏
  5. 看系统日志 —— 尤其是 lowMemoryKiller、watchdog、dropbox 的输出

最后一句话:Crash 不可怕,可怕的是看不懂堆栈。保持对堆栈的敏感度,多积累常见模式,你也能一眼看出病因。我习惯在工程里建一个「Crash 知识库」,每次遇到新类型就记录特征和修复方案——这是性价比最高的自我迭代方式。