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 空指针异常可分为三大根因分支:
判断规则:堆栈中出现
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;
}
}
}
复测
修复后的验证步骤:
- 安装测试:使用
adb install部署修复版本 - 压力测试:循环执行编解码 + 释放操作 500 次以上
- 并发触发:在快速切换页面或连续开闭解码器场景下验证
- 日志断言:在回调入口和释放点加入日志,确认无空引用通过保护点
通过 dumpsys media.codec 查看 MediaCodec 实例是否全部正确释放:
adb shell dumpsys media.codec --list
# 返回结果中不应有泄露的实例
预防
- 统一生命周期管理:使用状态机包装类强制状态转换,避免直接调用 MediaCodec API
- 空引用注解:使用
@Nullable和@NonNull注解标注所有 MediaCodec 相关参数 - 静态分析工具:集成 FindBugs 或 SpotBugs,自动检测可能的 NPE 路径
- 单元测试:编写多线程并发释放的回调测试用例,覆盖异步场景
关键习惯:在每一个异步回调入口处,对外部对象的引用做一次显式的 null 检查(非 assert),这是最廉价且最有效的防御手段。