常见 Crash 类型:空指针、数组越界、内存泄漏

做 Mock 回放测试这几年,我遇到最多的 Crash 就那几种。说白了,大多数崩溃都逃不过空指针、数组越界和内存泄漏这三座大山。你想想看,一个空对象调方法、一个数组下标跑飞了、一个不停申请内存却不释放,哪个不是家常便饭?

我习惯先把 Crash 按类型分类,再针对每种类型设计回放场景和断言。这样测试覆盖率才够,排查定位也快。下面我逐个聊聊,顺带分享一些当年踩过的坑。

1. 空指针异常(NullPointerException)

空指针是 Java/Kotlin 开发者最熟悉的噩梦。原因很简单:某个对象值为 null,却调用了它的方法或属性。在 Mock 回放中,我经常故意把响应里的字段置空,看看代码能不能扛住。

核心触发场景:

  • 网络返回 JSON 中某个字段缺失或为 null
  • 数据库查询返回空对象
  • 链式调用中间某一步返回 null
  • 第三方 SDK 回调参数为 null
// 典型空指针示例
public void processUser(User user) {
    String name = user.getName(); // 这里可能抛 NullPointerException
    System.out.println(name.toUpperCase());
}

// Mock 回放时的测试数据:user 对象或 user.getName() 返回 null
{
  "user": null
}

我个人习惯在 Mock 数据里给每个对象都准备一个“全 null 版本”。有一次线上事故,就是因为后台字段名改了,客户端没更新,整个详情页白屏。从那以后,我坚持在回放测试中覆盖所有字段为 null 的情况。

避坑指南

我曾经犯过一个低级错误:只把顶级对象置 null,但忽略了嵌套对象内的字段。比如 User 不为 null,但 User.address 是 null。这种“半空”情况更难排查。建议你写个脚本,自动生成各层级的 null 组合。

2. 数组越界(IndexOutOfBoundsException)

数组越界通常出现在列表遍历、下标计算、或者是解析定长协议时。Mock 回放中,我最喜欢干的事就是故意让数据量超出预期——比如服务器返回了 100 条,但客户端只给列表预分配了 10 个位置。

高频触发点:

  • 用 fori 循环遍历 List,但下标 i 跑到了 size 之外
  • 取子列表时 start > end 或 start < 0
  • 数组长度或 size 被篡改为负数
  • 并发修改导致 size 和实际元素数不一致
// 数组越界典型场景
List<String> items = mockResponse.getItems();
for (int i = 0; i <= items.size(); i++) { // 注意:应该是 i < items.size()
    System.out.println(items.get(i)); // i == size 时越界
}

// Mock 数据:让 items.size() 返回 0,但代码里却用了 i <= size

我记得有一次排查一个崩溃,日志显示数组越界,但代码看起来没问题。后来发现是某个第三方库通过反射修改了 ArrayList 的 size 字段,导致表面 size 和实际容量不一致。所以我现在做 Mock 回放时,会模拟一些极端数据:比如 size 为 0、size 为 Integer.MAX_VALUE、甚至 size 为负数。

特别注意: 有些语言(如 C++)的数组越界不会立刻崩溃,而是会写坏内存,导致后续诡异问题。Mock 回放时最好配合 AddressSanitizer 或 Valgrind 来检测。

3. 内存泄漏(Memory Leak)

内存泄漏不像空指针那么直接崩溃,它是“温水煮青蛙”——你的 App 越来越卡,最后被系统杀掉。在 Mock 回放中,我主要关注以下几种常见的泄漏模式。

常见泄漏模式:

  • 长生命周期对象(如 Application)持有短生命周期对象(如 Activity)的引用
  • 静态集合类(如 HashMap、ArrayList)只增不减
  • 未取消的广播、观察者、回调
  • Handler、Timer 内部类持有外部引用
  • WebView 等重量级组件未被及时销毁
// 内存泄漏典型:非静态内部类隐式持有外部类引用
public class MainActivity extends AppCompatActivity {
    private Handler handler = new Handler() { 
        @Override
        public void handleMessage(Message msg) {
            // 这个 Handler 会隐式持有 MainActivity 的引用
            updateUI();
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 忘记移除所有回调会导致泄漏
        // handler.removeCallbacksAndMessages(null); // 这行被注释了
    }
}

我在一个项目中遇到过 UI 卡顿越来越严重,最后发现是某个 Fragment 的 View 被 Activity 的成员变量持有了,退出 Fragment 后一直没释放。回放测试时,我用 Mock 数据反复进出页面,然后用 MAT 或者 LeakCanary 检查,很快就发现了问题。

避坑指南

我曾经写过一段代码:在回调里执行网络请求,但忘记移除回调。结果每个请求都持有一个 Activity 的引用,导致泄漏。后来我养成了习惯:所有注册的监听器、回调、定时器,都在 onPause 或 onDestroy 里注销。Mock 回放时,我会模拟反复横跳场景,去验证这些清理逻辑是否真的执行。

4. 常见 Crash 类型对比

下面这张表格总结了我日常对付这三种 Crash 的要点,方便你快速对照。

Crash 类型 触发条件 Mock 回放策略 定位工具
空指针 对象/属性为 null 时被调用 构造全 null 树、部分 null 树 异常堆栈、IDE 的 Nullability 注解
数组越界 下标小于 0 或大于等于 size 设置负数、超大、空列表 异常堆栈、边界检查日志
内存泄漏 对象被持续引用无法 GC 重复进出页面、模拟大对象加载 LeakCanary、MAT、Profile

重要提醒: 这三种类型往往不是孤立的。比如空指针导致的 catch 块里可能又引发数组越界;内存泄漏造成的 OOM 又会触发其他异常。Mock 回放时最好组合使用异常数据,让你的代码在任何组合下都不“翻车”。

说到这,你可能已经看出来了——好的 Mock 回放测试,本质上就是制造一个“最坏世界”:所有接口返回 null,所有列表爆炸,所有资源不释放。只有在这种地狱级场景下还能稳如老狗的代码,才敢上生产。

接下来我给出一张流程图,把这三种 Crash 类型以及对应的检测思路串起来。平时做测试计划时,我会把这张图贴在墙上,提醒自己别漏了什么。

常见 Crash 类型与 Mock 回放检测流程 常见 Crash 类型 空指针 数组越界 内存泄漏 全 null / 部分 null Mock 响应插入 null 负数 / 超大 / 空列表 边界 + 并发修改 反复进出页面 未释放回调 / 集合 回放测试结果分析 定位修复 → 补充回归用例

嗯,这张图大概描述了我个人常用的 Crash 测试框架。从顶层三种类型,到具体模拟手段,最后汇总到分析修复。你可以在自己的测试方案里直接套用这个结构,也可以把具体工具名填到对应的框里。

实战建议

我建议你在每次迭代结束后,都针对这次新增的类和方法,按照上述三种类型补充对应的 Mock 回放场景。坚持几个版本下来,你会发现自己代码里那种“低级”崩溃越来越少。毕竟,最让人崩溃的 Crash,往往就是空指针这种“明明能提前测出来却漏了”的问题。

好了,这章的内容差不多就是这些。记住:空指针、数组越界、内存泄漏,它们是 Crash 界的“铁三角”。你在 Mock 回放中抓住了它们,至少 80% 的崩溃就能提前扼杀在摇篮里。