视频解码优化:硬解与软解的选择策略、MediaCodec异步模式、解码器生命周期管理
解码这件事,说白了就是把压缩的视频数据还原成我们能看的画面。Android 上做解码,绕不开两个选择:硬解还是软解?用同步还是异步?解码器什么时候创建、什么时候销毁?
这些问题,我在项目里都踩过坑。今天咱们就一个一个掰开聊。
硬解 vs 软解:到底选哪个?
先说说硬解和软解的本质区别。
硬解,就是让 GPU 或者专用的解码芯片去干活。它省电、效率高,但兼容性是个老大难。不同厂商、不同芯片,对格式的支持千差万别。我遇到过某款国产手机,硬解 H.265 10bit 直接花屏,换成软解就没事。
软解,就是用 CPU 去算。它兼容性好,什么格式都能解,但功耗高、发热大。你想想看,CPU 本来就要处理 UI、网络、业务逻辑,再让它去解码,手机不烫才怪。
那到底怎么选?我个人的习惯是:
- 优先尝试硬解,毕竟省电、流畅。
- 硬解失败或花屏,立即降级到软解。
- 后台播放或小窗播放,用软解更稳妥。
- 直播场景,硬解延迟更低,优先选硬解。
核心原则:硬解是首选,软解是兜底。不要硬解一条路走到黑,也不要全程软解浪费电量。
我建议你在项目里做一个解码器探测机制。启动时先查一下设备支持的硬解格式,列个白名单。如果不在白名单里,直接走软解。这样可以避免运行时突然花屏或者崩溃。
| 对比维度 | 硬解 | 软解 |
|---|---|---|
| 功耗 | 低 | 高 |
| 兼容性 | 差(依赖芯片) | 好 |
| 延迟 | 低 | 较高 |
| 发热 | 低 | 高 |
| 适用场景 | 主播放、直播 | 后台、小窗、兜底 |
MediaCodec 异步模式:别再死循环了
很多人用 MediaCodec 还是老一套——开个线程,死循环里调 dequeueOutputBuffer。嗯,这种写法在早期 Android 版本上确实能用,但到了 Android 5.0 以后,官方就推荐用异步模式了。
为什么?因为同步模式会一直占用一个线程,而且你还要自己处理各种状态切换。异步模式呢,MediaCodec 会通过回调告诉你:有数据了、格式变了、解码完成了。你只需要实现回调接口就行。
我给你们看一段我项目里用的异步模式代码:
MediaCodec codec = MediaCodec.createDecoderByType(mimeType);
codec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 拿到输入缓冲区,塞数据进去
ByteBuffer inputBuffer = codec.getInputBuffer(index);
// 从网络或文件读取数据,写入 inputBuffer
codec.queueInputBuffer(index, 0, size, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
// 拿到解码后的数据,渲染或者处理
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
// 渲染到 Surface 或者做后续处理
codec.releaseOutputBuffer(index, true);
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 解码器输出格式变了,比如分辨率变化
// 更新渲染器配置
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 解码出错,做降级处理
}
});
codec.configure(format, surface, null, 0);
codec.start();
你看,代码结构清晰多了。不用自己维护线程,不用死循环,回调来了就处理。而且异步模式在低端机上表现更好,因为不会一直占着 CPU 不放。
小技巧:异步模式下,onInputBufferAvailable 和 onOutputBufferAvailable 是在同一个线程里回调的。不要在回调里做耗时操作,否则会阻塞解码流程。如果需要做复杂处理,丢到子线程去。
解码器生命周期管理:创建、复用、销毁
解码器不是创建了就能一直用的。它的生命周期管理,直接影响到内存和性能。
我见过不少项目,每次播放都 new 一个 MediaCodec,播放完就扔那不管了。结果呢?内存泄漏、解码器耗尽、崩溃。Android 系统对 MediaCodec 实例数量是有限制的,一般同时只能有 8-16 个。
正确的做法是这样的:
- 创建:在需要解码时创建,不要提前创建。创建时指定好 MIME 类型和 Surface。
- 配置:调用
configure后,一定要调start才能开始工作。 - 使用:异步模式下,通过回调处理输入输出。
- 停止:调用
stop后,解码器会清空所有缓冲区。 - 释放:调用
release释放资源。这一步不能省。
注意:MediaCodec 一旦 stop 后,就不能再 start 了。如果你想复用,必须重新 configure 再 start。但说实话,我建议直接释放旧的,创建新的。复用带来的性能提升微乎其微,反而容易出 bug。
我曾经在一个直播项目里,因为解码器没释放,导致切台 10 次后 app 直接闪退。查了半天才发现是 MediaCodec 实例数超限了。从那以后,我就在每个解码器使用完后,在 finally 块里确保调用 release。
还有一个坑:解码器状态切换。MediaCodec 有多个状态:Stopped、Configured、Executing、Flushed 等。你在调用 queueInputBuffer 之前,一定要确保解码器处于 Executing 状态。否则会抛 IllegalStateException。
我建议你封装一个解码器管理器,统一管理创建、使用、释放。这样代码更干净,也不容易漏掉释放。
public class DecoderManager {
private MediaCodec codec;
public void init(String mimeType, Surface surface) {
codec = MediaCodec.createDecoderByType(mimeType);
codec.setCallback(new DecoderCallback());
codec.configure(format, surface, null, 0);
codec.start();
}
public void release() {
if (codec != null) {
codec.stop();
codec.release();
codec = null;
}
}
}
知识体系总览
下面这张图,把解码优化的核心逻辑串起来了。你可以对照着看,心里有个谱。
嗯,解码优化这块,说白了就是三个事:选对方式、用对模式、管好生命周期。你把这三点吃透了,大部分解码性能问题都能搞定。
最后说一句:不要迷信硬解,也不要排斥软解。根据场景灵活切换,才是真正的优化。我在项目里做过一个自动降级策略:硬解连续失败 3 次,自动切软解,并且记录设备信息,下次直接走软解。效果还不错。