4. 空指针异常(NullPointerException)排查:判断空引用来源、对照变量生命周期

现象识别

典型堆栈特征:空指针异常在 MediaCodec 调用栈中通常是最高频的崩溃之一。堆栈顶部往往指向对 MediaCodec 实例、BufferInfo 或 ByteBuffer 引用的直接调用处。

java.lang.NullPointerException: Attempt to invoke virtual method 'android.media.MediaCodec$BufferInfo android.media.MediaCodec.getOutputBufferInfo(int)' on a null object reference
    at com.example.media.MediaCodecDecoder.drainOutputBuffer(MediaCodecDecoder.java:142)
    at com.example.media.MediaCodecDecoder.onFrameAvailable(MediaCodecDecoder.java:98)
    at android.media.MediaCodec$Callback.onOutputBufferAvailable(MediaCodec.java:3502)
    ... 15 more
识别要点:堆栈中 on a null object reference 之前的方法名即为空引用的直接调用点;从 drainOutputBuffer → onFrameAvailable → MediaCodec.Callback 的层级可判断是异步回调中 MediaCodec 实例已被释放。

数据采集

定位空引用来源需要采集以下维度数据:

  • 变量声明位置:成员变量、局部变量还是静态变量
  • 最后一次非空赋值点:通过日志或断点记录
  • 生命周期同步点:释放(release())与回调的并发时序
  • 异步线程调用栈:完整捕获所有线程的堆栈信息
采集项 目标 典型工具/方法
变量作用域 判断是误用还是生命周期结束 代码审查(Code Review)
引用赋值时间轴 确认变量是否被二次置空 日志埋点 + 时间戳
并发线程堆栈 找出释放与调用的竞争点 traceview / adb shell dumpsys
MediaCodec 状态机 确定对象是否在合法状态 状态枚举 + 断言

原因定位 — 故障树分析

按照故障树自上而下的逻辑,MediaCodec 空指针异常可分为三大根因分支:

MediaCodec空指针异常故障树 MediaCodec 空指针异常 对象未初始化 状态错误使用 异步回调空引用 createByCodecName 未调用 构造函数中未赋值 configure 前 start release 后继续调用 状态枚举未同步 外部引用被置 null BufferInfo 数组未初始化 判断规则 • 若堆栈中 MediaCodec 对象本身为 null → 分支一(未初始化) • 若对象非 null 但调用被状态机拒绝 → 分支二(状态错误) • 若回调中引用外部对象为 null → 分支三(异步回调空引用)
判断规则:堆栈中出现 getInputBuffer()、getOutputBuffer()、dequeueOutputBuffer() 等方法,且 on a null object reference 指向 android.media.MediaCodec 本身 → 绝大多数属于“对象未初始化”分支;若指向的是内部回调中的外部引用(如解码器封装类)→ 走“异步回调空引用”分支。

对照变量生命周期

将变量生命周期与 MediaCodec 状态机对齐是定位空引用的关键。以下为生命周期对照规则:

变量 合法生命周期 常见空引用时机
MediaCodec 实例 create → release 在 release 后仍持有引用并调用
BufferInfo 数组 configure 后首次 dequeue 前 未调用 getOutputBufferInfo() 前就被访问
ByteBuffer 引用 dequeueOutputBuffer 成功 → 调用 releaseOutputBuffer 前 连续两次 dequeue 而中间释放了 buffer
外部回调封装类 整个解码生命周期 异步线程中封装类被置 null
// 错误示例:回调中外部引用被置空
private MediaCodecDecoder decoder; // 可能被外部置 null

private MediaCodec.Callback callback = new MediaCodec.Callback() {
    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
        // 此处 decoder 可能为 null
        decoder.drainOutputBuffer(codec, index, info); // NPE!
    }
};

public void releaseDecoder() {
    if (decoder != null) {
        decoder.release();
        decoder = null; // 置空后回调线程仍可能尚未退出
    }
}

修复动作

根据故障树分支,采取对应修复策略:

  • 分支一(未初始化):在构造函数或初始化方法中确保 createByCodecName 或 createDecoderByType 被调用,并检查返回值。
  • 分支二(状态错误):每次调用操作前增加状态断言,确保当前状态为 Configured、Running 或 Flushed。
  • 分支三(回调空引用):在回调入口处做空引用检查,或将释放逻辑与回调线程同步(如使用 Handler + Looper 确保顺序执行)。
// 修复后:回调中增加空引用保护 + 同步释放
private final Object lock = new Object();
private MediaCodecDecoder decoder;

private MediaCodec.Callback callback = new MediaCodec.Callback() {
    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
        MediaCodecDecoder d;
        synchronized (lock) {
            if (decoder == null) return; // 空引用保护
            d = decoder;
        }
        d.drainOutputBuffer(codec, index, info);
    }
};

public void releaseDecoder() {
    synchronized (lock) {
        if (decoder != null) {
            decoder.release();
            decoder = null;
        }
    }
}

复测

修复后的验证步骤:

  1. 安装测试:使用 adb install 部署修复版本
  2. 压力测试:循环执行编解码 + 释放操作 500 次以上
  3. 并发触发:在快速切换页面或连续开闭解码器场景下验证
  4. 日志断言:在回调入口和释放点加入日志,确认无空引用通过保护点

通过 dumpsys media.codec 查看 MediaCodec 实例是否全部正确释放:

adb shell dumpsys media.codec --list
# 返回结果中不应有泄露的实例

预防

  • 统一生命周期管理:使用状态机包装类强制状态转换,避免直接调用 MediaCodec API
  • 空引用注解:使用 @Nullable 和 @NonNull 注解标注所有 MediaCodec 相关参数
  • 静态分析工具:集成 FindBugs 或 SpotBugs,自动检测可能的 NPE 路径
  • 单元测试:编写多线程并发释放的回调测试用例,覆盖异步场景
关键习惯:在每一个异步回调入口处,对外部对象的引用做一次显式的 null 检查(非 assert),这是最廉价且最有效的防御手段。