深入 Android SurfaceView / TextureView / SurfaceTexture 全链路:从 BufferQueue 生产端到 Surface 合成路径的渲染桥接架构
有一次做视频通话优化,同一路 720p 视频流,在 SurfaceView 上稳定 60 帧,换到 TextureView 直接掉到 45 帧,还多了 20ms 延迟。当时第一反应是 TextureView 用得不对,查了三天才发现问题不在业务层,而在两条完全不同的合成路径。这篇文章从 BufferQueue 的生产-消费模型出发,把这三者的桥接关系拆开讲。
BufferQueue:Surface 的本质是生产者句柄
Android 图形栈的核心不是 SurfaceView,而是 BufferQueue。它是一套生产者-消费者队列:生产者提交 GraphicBuffer,消费者取走渲染,用完归还到缓冲池。
应用侧拿到的 Surface,本质上是 IGraphicBufferProducer 的 Binder 代理。你往 Surface 上画内容,实际是在往 BufferQueue 里 enqueue buffer。
消费端是谁,决定内容去向。SurfaceFlinger 是最常见的消费者:它 acquire 每个窗口的 buffer,交给 Hardware Composer(HWC)合成上屏。如果应用自己用 SurfaceTexture 消费,buffer 就转成 GL 纹理,再走应用自己的绘制管线。
理解这一点后,SurfaceView 和 TextureView 的差异就清楚了:一个把 Surface 直接交给 SurfaceFlinger,一个把 Surface 交给 SurfaceTexture 再画进主窗口。
SurfaceView:独立 Layer 的打洞合成
SurfaceView 初始化时,会向系统申请一块独立 Surface,注册成 SurfaceFlinger 的一个独立 Layer。这个 Layer 不经过应用主窗口的 EGL 渲染,直接从生产者到合成器。
所以 SurfaceView 上的视频内容,与应用 UI 是两条并行管线。主窗口在上层,SurfaceView 的 Layer 在下层,主窗口挖一个透明”洞”来显示下层内容,这就是常说的 punch-through 机制。
val surfaceView = SurfaceView(context)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
player.setSurface(holder.surface) // Surface 直接交给播放器
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {}
})
独立 Layer 带来两个直接收益:帧率可控,可以单独设置刷新率;延迟更低,buffer 不用在主窗口里多做一次纹理拷贝。
代价同样明显。因为是打洞,SurfaceView 无法真正参与 View 树的绘制变换。你旋转父 View,主窗口动了,下层 Surface 内容不会同步变换,早期 Android 版本甚至会出现内容跑出洞外的穿帮。滚动列表里放 SurfaceView,内容会滞后于页面滚动,这是很多人踩过的坑。
SurfaceTexture:把生产端 buffer 转成 GL 纹理
SurfaceTexture 是应用侧作为消费者的封装,内部持有 IGraphicBufferConsumer。它只做一件事:接收 BufferQueue 里的 buffer,通过 EGLImage 绑定成 OpenGL ES 外部纹理(GL_TEXTURE_EXTERNAL_OES)。
val surfaceTexture = SurfaceTexture(texName).apply {
setOnFrameAvailableListener { onFrameAvailable() }
}
val surface = Surface(surfaceTexture) // 给生产者用,例如 Camera2 或 MediaCodec
cameraDevice.createCaptureSession(listOf(surface), ...)
生产者写入新帧后触发回调,你在 GL 线程里调用 updateTexImage():
override fun onDrawFrame(gl: GL10?) {
surfaceTexture.updateTexImage() // 把最新 buffer 绑定到当前 OES 纹理
// 用该纹理绘制
}
相机预览和视频处理基本都走这条路。Camera2 没有”直接给个 View”的 API,它只认 Surface,你给 SurfaceTexture 还是 SurfaceView 的 Surface 都可以。
SurfaceTexture 本身不负责显示,只负责把 buffer 变成纹理。画到 GLSurfaceView 是一条 GL 渲染链路,画到 TextureView 则是下面这条合成路径。
TextureView:绕回主窗口的纹理绘制
TextureView 是 SurfaceTexture 的一个典型宿主。它内部持有 SurfaceTexture,对外表现是一个普通 View。
关键流程是:生产者往 Surface 写 buffer → SurfaceTexture 转成 OES 纹理 → TextureView 在硬件加速绘制时,把这个纹理作为 View 的一部分画进主窗口 Layer → SurfaceFlinger 统一合成上屏。
val textureView = TextureView(context)
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
player.setSurface(Surface(st))
}
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) {}
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean = false
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
}
多出来的这一跳有可见成本。buffer 通过 EGLImage 绑定成纹理这一步在大多数设备上是零拷贝的,真正的开销是纹理还要作为 View 的一部分再绘制进主窗口、多走一次合成。我在同一台设备上实测,视频通话场景 TextureView 比 SurfaceView 平均多 15-25ms 渲染延迟,GPU 占用也更高。
它的价值在 UI 一致性。TextureView 的内容就是 View 树的一部分,缩放、旋转、透明度、截图、共享元素转场全部天然生效。产品要求视频区域做圆角、跟手势缩放,TextureView 是成本最低的选择。
相机与视频渲染怎么选
这三个组件不是同一层级。SurfaceTexture 是数据形态转换器,SurfaceView 和 TextureView 是两种显示终端。相机预览和视频渲染的选型,本质是在低延迟直通和 UI 灵活之间取舍。
我的判断标准就一条:内容是否需要参与 UI 变换。
不需要变换、追求极致低延迟,选 SurfaceView。典型场景是播放器全屏页、相机取景、游戏画面,这些场景内容独立于 UI 布局,打洞的劣势基本感受不到。
需要动画、圆角、复杂层级,选 TextureView。典型场景是信息流内嵌视频、带手势缩放的相机预览页,多出的 15ms 在这些场景里通常可接受。
需要在帧上做滤镜、美颜、算法处理,直接用 SurfaceTexture 接 Camera2 输出,处理完再画到 GLSurfaceView 或编码器。别先接 TextureView 再从 View 里往回取帧,那样会白白多走一趟主窗口合成。
还有一个点容易踩坑:SurfaceTexture 默认是异步多缓冲模式,单缓冲要显式传 true 才开启;updateTexImage 调得慢就会丢帧。onFrameAvailable 回调默认投递到创建线程,往往是主线程,别在回调里做重活,把耗时处理挪到渲染线程。