Crash 产生原因分析

聊完了 Crash 和 ANR 的区别,咱们得扎进具体的 Crash 种类里看看。说实话,我干了这么多年 Android 开发,遇到过最多、最让人头疼的就是那几种老面孔。今天咱们就把它们挨个掰开揉碎,看看背后到底什么鬼,以及怎么从源头堵住它们。

常见 Crash 类型与根因分析 常见 Crash 类型与根因分析 常见 Crash 类型 NullPointerException IndexOutOfBoundsException ClassCastException 未判空引用 链式调用断裂 下标越界 容量未校验 类型向下转换错误 泛型擦除误导 预防 & 监控:判空检查 · 边界校验 · instanceof 判断 · 静态扫描 · 埋点采集

NullPointerException —— 最熟悉的陌生人

说实话,NPE 在 Crash 排行榜上常年霸占第一。我个人习惯在写代码时只要看到可能为 null 的对象,就下意识加个判空。但你想想看,很多同学明明知道这个道理,代码里还是经常 “嘭” 一下炸掉。

为什么会这样?归根结底就两种场景:

  • 链式调用中断了——比如 user?.getAddress()?.getCity(),如果你用了 Java 而不是 Kotlin,中间任何一个环节返回 null,直接就 NPE。
  • 第三方 SDK 返回空数据未处理——我记得有一次做 IM 聊天,对方服务器偶尔返回一个空的 profile 对象,结果我们没判空直接拿头像 URL 去加载图片,线上瞬间爆了一堆 NPE。
✍️ 我的经验:写代码时养成“先假设 null,再验证非空”的习惯。用 Optional 或 @Nullable 注解都能帮你提前发现风险。

一个典型例子:

// 不安全的写法
String city = user.getAddress().getCity(); 
// 如果 user 或 getAddress() 为 null → 崩

// 安全的写法
if (user != null && user.getAddress() != null) {
    String city = user.getAddress().getCity();
}

IndexOutOfBoundsException —— 下标玩脱了

这个异常我也踩过不少坑。说白了就是对列表、数组的访问越界。最常见的就是遍历时删除了元素,或者从网络拿到的数据数量和预想的不一致。

我一个项目里遇到过这个 bug:列表有 10 项,后台列表新增了一个隐藏项(用户不可见),但前端分页时按之前 10 项的大小取数据,结果 list.get(10) 直接越界。后来我给所有集合操作都加了边界判断。

避免越界的几个原则:

  1. 用增强 for 循环或 for (int i = 0; i < list.size(); i++) 时,循环条件一定要基于 size() 动态计算。
  2. 执行 list.get(index) 前,确认 index >= 0 && index < list.size()。
  3. 使用 List.subList() 后,不建议对原列表做结构性修改(如 add/remove),极易触发 ConcurrentModificationException 或者 IndexOutOfBoundsException。
⚠️ 曾经踩过的坑:千万不要在 RecyclerView.Adapter 的 onBindViewHolder 中直接 get(position) 而不过滤,特别是在列表有 header/footer 或者局部刷新时,position 很容易与实际数据源索引对不上。

ClassCastException —— 类型强转的雷

这个异常说白了就是你想把一只猫当狗用。强转时类型不匹配,运行时直接 Crash。尤其是在使用 ArrayList 转 MyModel 时,或者从 JSON 反序列化得到的对象类型与预期不一致。

我的一个惨痛经历:早期用 Gson 解析接口,后端返回的某个字段有时是 JSONObject 有时是 JSONArray。我没加 instanceof 判断直接强转成 List,结果线上部分用户数据直接崩溃。后来加了类型校验才解决。

正确的做法:

Object data = response.get("data");
if (data instanceof List) {
    List<MyModel> list = (List<MyModel>) data;
    // 处理列表
} else if (data instanceof Map) {
    MyModel model = new MyModel((Map) data);
    // 处理单条
} else {
    // 兜底处理
}

另外,泛型擦除也是个隐形杀手。你在代码里写了 List<String>,编译后变成 List,运行时拿到的元素类型未检查就直接强转,很容易翻车。所以从外部(比如 Intent 传递的 Serializable、Parcelable)取数据时,一定要做类型校验。

总结表格:三类常见 Crash 对比

Crash 类型 典型触发场景 根因 推荐预防手段
NullPointerException 未判空调用方法/访问属性 对象引用为 null 判空检查、Optional、注解
IndexOutOfBoundsException 数组/列表下标越界 index ≥ size 或 index < 0 边界判断、使用增强 for
ClassCastException 不安全的向下转型或泛型误用 运行时类型不匹配 instanceof 检查、泛型校验

核心思路:这三种 Crash 其实都有很清晰的触发模式。我们只要能识别出“可疑代码段”,加一道防御判断,就能拦截大部分线上问题。顺便说一句,我们还可以在关键路径上埋点,用 Crash 采集工具把堆栈信息实时上报,这样即使漏网了也能快速定位。