18、硬件加速与同步:MediaCodec硬件解码、SurfaceView/TextureView渲染、GPU合成时序
各位同学,今天我们来聊聊硬件加速这条“快车道”上的同步问题。
说实话,软件解码在低分辨率时代还能凑合。但到了4K、8K,甚至高帧率HDR,CPU软解就是一条死路。我个人习惯,只要目标设备支持,一律走硬件解码。但硬件加速不是白给的——它把解码、渲染、合成这三件事扔给了不同的硬件单元,同步的复杂度一下子就上来了。
18.1 MediaCodec硬件解码:从CPU到GPU的“快递”
MediaCodec开启硬件解码很简单,但背后的数据流很多人没搞清。
你调用 configure(format, surface, null, 0) 时,如果传了一个Surface进去,MediaCodec就会直接输出到GPU纹理,绕过了CPU。这就是所谓的“零拷贝”路径。我在项目中遇到过,有些同学以为传了Surface就万事大吉,结果发现画面卡顿——其实是因为没处理好同步。
那怎么拿到解码后的数据呢?
- Surface模式: 输出直接到Surface,适合直接显示或传给MediaCodec的输入Surface做转码。
- ByteBuffer模式: 输出到CPU内存,但硬件解码下这个操作开销很大,不推荐实时场景。
嗯,这里要注意:硬件解码器内部维护了一个Buffer队列。你调用 dequeueOutputBuffer() 时,返回的index对应的Buffer,在Surface模式下其实是一个“空壳”——真正的数据已经在GPU里了。你只需要把它交给渲染器就行。
18.2 SurfaceView vs TextureView:渲染容器的选择
这两个东西,说白了就是Android提供的两个“画板”。但它们的底层机制完全不同。
| 特性 | SurfaceView | TextureView |
|---|---|---|
| 独立窗口 | 是(有独立的Surface) | 否(嵌入View层级) |
| 硬件合成 | 支持(直接走HWC) | 不支持(需GPU合成) |
| 动画/变换 | 困难(需额外处理) | 容易(支持View动画) |
| 同步复杂度 | 低(独立时序) | 高(需与UI线程同步) |
我个人习惯:如果只是全屏播放视频,无脑选SurfaceView。它有一个独立的合成层,硬件合成器(HWC)可以直接处理,延迟最低。但如果你要做弹幕、缩放、旋转这些花哨效果,TextureView更灵活——代价是它需要GPU把内容合成到View层级里,多了一步。
18.3 GPU合成时序:谁先谁后?
现在我们把解码和渲染连起来。硬件解码输出到Surface,Surface背后是一个BufferQueue。消费者(SurfaceView/TextureView)从队列里取Buffer,然后交给GPU合成。
问题来了:GPU什么时候开始合成?
答案是:等VSYNC信号。Android的渲染管线是双缓冲甚至三缓冲的。GPU合成器(HWC或GPU本身)会在每个VSYNC周期开始前,拿到当前可用的最新Buffer,然后合成并送显。
这里有个经典的坑:解码器输出太快,渲染器消费太慢。BufferQueue会堆积,导致延迟越来越高。反过来,如果解码器输出太慢,渲染器会饿死,画面卡顿。
怎么同步?我常用的方法是:
- 时间戳传递: 解码器输出的每个Buffer都带有一个PTS(显示时间戳)。渲染器在合成时,根据PTS决定是否显示。
- 丢帧策略: 如果解码器堆积了太多Buffer,且当前PTS已经落后于系统时钟,直接丢弃旧Buffer,只显示最新的。
- 动态调整: 如果发现渲染器跟不上,可以降低解码器的工作频率(比如跳帧解码)。
你想想看,如果解码器输出的是30fps,但屏幕刷新率是60Hz,那每个Buffer应该显示2个VSYNC周期。但如果解码器突然输出一个60fps的流,渲染器就必须在1个VSYNC内完成合成——这时候GPU的压力就大了。
18.4 实战:MediaCodec + SurfaceView 的同步代码骨架
光说不练假把式。我贴一段核心代码,展示如何把硬件解码和SurfaceView串起来,并做基本的同步。
// 初始化解码器,输出到Surface
MediaCodec decoder = MediaCodec.createDecoderByType(mimeType);
decoder.configure(format, surface, null, 0);
decoder.start();
// 渲染循环(在单独线程中)
while (!isStop) {
int index = decoder.dequeueOutputBuffer(bufferInfo, 10000);
if (index >= 0) {
// 获取PTS
long ptsUs = bufferInfo.presentationTimeUs;
// 判断是否该显示
if (ptsUs > getCurrentTimeUs()) {
// 还没到时间,等待
Thread.sleep((ptsUs - getCurrentTimeUs()) / 1000);
} else if (ptsUs < getCurrentTimeUs() - 50000) {
// 落后太多,丢帧
decoder.releaseOutputBuffer(index, false);
continue;
}
// 渲染(Surface模式下,releaseOutputBuffer(true)就会送显)
decoder.releaseOutputBuffer(index, true);
}
}
releaseOutputBuffer(index, true) 的第二个参数表示是否渲染。传true,Buffer就会在下一个VSYNC被合成。
这段代码看起来简单,但实际项目中要考虑更多:
- 如果解码器输出的是HDR内容,GPU合成时需要做色调映射。
- 如果SurfaceView被遮挡或不可见,应该暂停渲染,避免浪费GPU资源。
- 多路视频合成时,需要为每一路维护独立的PTS队列,并在GPU端做混合。
18.5 总结:硬件加速下的同步本质
说白了,硬件加速并没有改变同步的本质——它只是把同步的战场从CPU转移到了GPU。你需要关心的不再是“CPU什么时候解码完”,而是“GPU什么时候合成完”。
我个人经验是:永远不要假设硬件解码器的输出节奏是稳定的。它可能因为温控、功耗、带宽等原因突然变慢。你的同步逻辑必须能自适应。
最后,留一个思考题:如果使用TextureView,并且你在上面叠加了一个自定义的OpenGL渲染层,那么GPU合成时序会变成什么样?嗯,这个问题留给你自己去挖坑填坑吧。