3、Surface与纹理:Surface输入与输出、SurfaceView与TextureView对比、OpenGL ES纹理桥接、GPU直接渲染
各位同学,今天我们来聊聊Android视频硬解码里最核心的“最后一公里”——解码后的数据怎么送到屏幕上。说白了,就是Surface和纹理的那些事儿。
我刚开始做视频播放时,总觉得解码完就万事大吉了。结果画面就是出不来,或者卡成PPT。后来才明白,解码只是前半程,怎么把数据高效地交给GPU渲染,才是真正的技术活。
3.1 Surface:解码器的“输出口”与显示层的“输入口”
Surface是什么?你可以把它想象成一个“数据管道”的接口。MediaCodec解码器输出数据,Surface就是那个出口。而显示层要消费数据,Surface又是入口。
我个人习惯把Surface理解成“生产者和消费者之间的共享缓冲区”。解码器往里面写数据,SurfaceFlinger或OpenGL ES从里面读数据。两边不用直接打交道,各干各的。
核心要点:Surface本身不存储图像数据,它只是一个句柄,指向一个BufferQueue。真正干活的是BufferQueue里的GraphicBuffer。
为什么会这样设计?你想想看,解码器和渲染器可能跑在不同的线程,甚至不同的进程。如果直接共享内存,同步问题会让人崩溃。Surface这套机制,说白了就是帮我们管理好了缓冲区的生命周期。
3.2 SurfaceView vs TextureView:我该怎么选?
这是个经典问题了。很多新手会纠结,我到底用哪个?嗯,这里我直接给结论:
| 对比维度 | SurfaceView | TextureView |
|---|---|---|
| 渲染方式 | 独立窗口,单独Layer | 与普通View共享窗口 |
| 性能 | 高,直接走硬件合成 | 中等,需要额外拷贝 |
| 动画/变换 | 不支持(需额外处理) | 支持,像普通View一样 |
| 截图/录屏 | 困难 | 容易 |
| 适用场景 | 全屏视频、高帧率游戏 | 需要动画、缩放、弹幕叠加 |
我在项目中遇到过这样一个坑:用SurfaceView做视频通话,结果因为SurfaceView有独立的窗口,导致上层UI的动画被遮挡。后来换成TextureView,虽然性能稍微降了一点,但UI交互流畅多了。
我的建议:如果你只是做全屏播放器,无脑选SurfaceView。如果你需要叠加复杂的UI动画、做画中画、或者需要截图功能,TextureView更合适。
3.3 OpenGL ES纹理桥接:把Surface变成纹理
有时候,我们不想直接把画面渲染到屏幕上,而是想先拿到数据做处理——比如加滤镜、做美颜。这时候就需要把Surface和OpenGL ES纹理桥接起来。
怎么做?核心就两步:
- 创建一个SurfaceTexture,而不是直接创建Surface。
- 把SurfaceTexture传给MediaCodec作为输出目标。
SurfaceTexture是什么?它实现了Surface的接口,但内部把数据输出到了一个OpenGL ES纹理上。这样,解码器以为自己在往Surface写数据,实际上数据进了纹理。
// 创建SurfaceTexture,绑定到纹理ID
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
Surface surface = new Surface(surfaceTexture);
// 配置MediaCodec输出到Surface
MediaCodec codec = MediaCodec.createDecoderByType(mimeType);
codec.configure(format, surface, null, 0);
codec.start();
// 在渲染循环中更新纹理
surfaceTexture.updateTexImage();
// 此时textureId上就有了解码后的视频帧
我记得第一次调通这个流程时,看到滤镜效果实时作用在视频上,那种成就感还是很爽的。不过要注意,updateTexImage()必须在GL线程调用,否则会崩溃。
避坑指南:我曾经在异步线程里调了updateTexImage(),结果画面一直黑屏。查了半天才发现是线程问题。记住:所有OpenGL ES操作必须在同一个GL线程。
3.4 GPU直接渲染:绕过CPU,效率拉满
为什么要搞GPU直接渲染?说白了,就是不想让CPU掺和进来。视频数据从解码器出来,如果能直接送到GPU显存里,然后由GPU渲染到屏幕,全程不走CPU内存,那效率是最高的。
这个流程是这样的:
- MediaCodec解码出一帧数据,放到GraphicBuffer里。
- 这个GraphicBuffer实际上就是一块GPU显存。
- SurfaceFlinger或者OpenGL ES直接引用这个Buffer,做合成或渲染。
- 整个过程,CPU只负责控制流,不碰像素数据。
你想想看,如果每帧数据都要从GPU显存拷贝到CPU内存,处理完再拷回去,那4K 60fps的视频带宽根本扛不住。GPU直接渲染,说白了就是零拷贝。
下面这张图展示了整个数据流转的核心逻辑:
从这张图可以看得很清楚:解码器出来的数据直接进了GPU显存,然后两条路——要么SurfaceFlinger直接合成,要么OpenGL ES做纹理处理。CPU在整个过程中只负责调度,不碰数据。
性能优化小技巧:如果你用OpenGL ES做纹理处理,尽量用GL_OES_EGL_image_external扩展。这个扩展允许你直接引用外部的GraphicBuffer,不需要拷贝到CPU内存。我实测过,4K视频用这个扩展,帧率能提升20%以上。
好了,这一章的内容就到这里。Surface和纹理的配合,说白了就是怎么让解码器和渲染器高效协作。理解了这些,你就能在性能和功能之间找到最佳平衡点。
本章小结:
- Surface是解码器输出和渲染器输入的桥梁,本质是BufferQueue的句柄
- SurfaceView性能好但灵活性差,TextureView相反,按场景选择
- SurfaceTexture可以把解码数据桥接到OpenGL ES纹理,实现滤镜等效果
- GPU直接渲染的核心是零拷贝,数据全程不经过CPU内存
公众号:蓝海资料掘金营,微信deep3321