Android SurfaceView & TextureView

1 阅读12分钟

它们都是用来在 Android UI 中显示复杂图形内容的组件,比如视频、相机预览或游戏画面,但实现方式和适用场景有很大不同。如果不了解他们的作用的话,在开发中很难去选择,下面就详细梳理下它们的区别和使用方式及其相关概念。

一、SurfaceView (表面视图)

SurfaceView 的核心思想是创建一个独立的、位于普通视图层级之外的 “表面” (Surface)

介绍

继承自 View,它包含两个部分——一个普通的 View 组件,用于在屏幕上占据位置,以及一个独立的 Surface。这个 Surface 拥有自己的绘图缓冲区,它不是在主 UI 线程上绘制的。因此,可以在一个单独的后台线程上向这个 Surface 渲染内容,而不会阻塞或影响主 UI 线程的流畅性。

一般的 Activity 包含的多个 View 会组成 View hierachy 的树形结构,只有最顶层的 DecorView,也即跟节点视图,才是对 WMS 可见。这个DecorView在WMS中有一个对应的WindowState。相应地,在SF中对应的Layer。而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。


Surface 绘制流程:

  1. ViewRootImpl 创建 SurfaceControl,交给 SurfaceFlinger 管理。
  2. SurfaceView 的 Surface 通过 SurfaceHolder 暴露给开发者。
  3. 可以用 CanvasOpenGL/MediaCodec独立线程绘制内容。
  4. SurfaceFlinger 直接将该 Surface 合成到屏幕(Overlay 或 GPU 合成)。

Z-Order: SurfaceView 的 Surface 通常会直接在窗口的背景上进行渲染,位于普通 View 的上面。你可以把它想象成一个“浮”在所有常规 View 之上的独立窗口层。

优点:

  • 性能优异:由于渲染发生在独立的线程和独立的表面上,它能高效处理大量、高频率的图形更新,非常适合实时渲染的场景,如视频播放相机预览游戏渲染
  • 不阻塞主线程:这是它最大的优势。即使你在 Surface 上进行高强度的绘制操作,比如每秒绘制 60 帧,主 UI 线程依然可以保持流畅,不会出现卡顿。
  • 硬件加速:通常可以利用硬件加速来提高渲染效率。利用硬件 Overlay,节省 GPU。
  • 使用双缓冲机制,播放视频时画面流畅。

缺点:

  • 无法变形和动画:因为它是一个独立的 Surface,它不受常规 View 属性的影响。无法像对待普通 View 一样对它进行旋转、缩放、透明度动画或平移。它基本上就是一个矩形区域。
  • 层级限制:它总是位于常规 View 的上方,这可能导致一些 UI 布局上的问题。你无法在 SurfaceView 上面放置另一个 View,除非你调整它的 Z-Order(但这比较复杂)。
  • 生命周期管理复杂:需要特别关注它的生命周期,例如在 surfaceCreated() 中开始渲染,在 surfaceDestroyed() 中停止渲染线程。
  • 透明处理有兼容性问题。

SurfaceView 中的双缓冲机制

什么是双缓冲?

想象一下你在画一幅画,如果直接在展示给观众的画板上作画,观众就会看到你作画的每一个笔触和修改过程,画面会不停闪烁、不完整,体验非常差。

双缓冲机制就是为了解决这个问题。它提供了两个“画板”:

  1. 后台缓冲区(Back Buffer) :这是一个你看不见的、位于内存中的画板。你所有的绘制操作,比如画线条、填充颜色、贴图等,都发生在这个缓冲区里。
  2. 前台缓冲区(Front Buffer) :这是正在屏幕上显示给用户看的画板。

双缓冲的工作流程大致如下:

  1. 锁定画布( lockCanvas()

当你的渲染线程需要开始绘制时,会调用 lockCanvas() 方法。这个方法会返回一个 Canvas 对象。这个 Canvas 对象实际上是与后台缓冲区关联的。此时,前台缓冲区的内容仍然在屏幕上显示,用户看到的还是上一帧的画面。

  1. 在后台绘制

现在,你可以在这个 Canvas 上进行任意的绘制操作,比如 drawBitmap()drawText()drawRect() 等。所有的这些操作都只在后台缓冲区中进行,屏幕上的画面不会有任何变化。这确保了用户看到的画面始终是完整的、稳定的。

  1. 提交并解锁画布( unlockCanvasAndPost(canvas)

当你完成所有的绘制操作后,会调用 unlockCanvasAndPost(canvas) 方法。这个操作非常关键,它会做两件事:

  • 缓冲区交换(Buffer Swap) :将绘制完成的后台缓冲区和正在显示给用户的前台缓冲区进行快速交换。这个操作通常由硬件完成,速度非常快。
  • 显示新画面:交换完成后,原本的后台缓冲区现在成了前台缓冲区,它的内容立即在屏幕上显示出来。而原来的前台缓冲区则成了新的后台缓冲区,等待下一帧的绘制。

在运用时可以理解为:SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布时,得到的实际上是backCanvas而不是正在显示的frontCanvas,之后你在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost(canvas)此视图,那么上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。

解决闪烁问题

如果没有双缓冲,你会直接在屏幕缓冲区上进行绘制。当你在绘制一个复杂图形(比如一个动画帧)时,绘制过程可能需要一段时间。在这个过程中,用户会看到部分旧画面和部分新画面混合在一起,导致画面出现闪烁(flicker) 撕裂(tearing)

双缓冲机制通过将绘制过程与显示过程完全分离,确保了用户看到的永远是完整的、已经绘制好的画面。整个画面更新是一个“原子操作”:在极短的时间内,整个画板被替换,而不是一点一点地修改。

应用场景

上面优点中已经简单描述了应用场景:视频播放相机预览游戏渲染等,拿相机举例,Android 的相机 API (CameraCamera2)可以直接将预览数据流传输到一个 Surface 上,而 SurfaceView 正好提供了这个 Surface。这是一种高效的硬件加速路径。

它们之间都有一个共同的特点:数据流是连续的,需要持续更新, 而普通 View 的 onDraw() 方法需要经过复杂的测量、布局和绘制流程,每次重绘的开销比 SurfaceView 要大得多,并且 渲染时没有双缓冲机制,如果绘制复杂图形,用户可能会看到绘制的中间过程,导致画面闪烁等等...这一系列缺点就让 SurfaceView 在这些场景下成为了唯一的选择!

如果你不知道什么时候使用它,就只要记住几个关键点即可:持续、高频率、高性能渲染的场景

现在想想之前做过一个运动类的 VIew,需要频繁刷新运动距离,使用普通 View 一帧一帧来刷,一卡一卡的就有点搞笑,还一直优化 onDraw 中的代码耗时,以为那就是性能的极限了,没想到那只是我的极限:)

使用示例

布局代码:

    <SurfaceView
        android:id="@+id/waveformSurfaceView"
        android:layout_width="0dp"
        android:layout_height="250dp"
        android:layout_margin="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

核心代码,Activity 包含 SurfaceHolder.Callback回调 和一个用于绘制的内部类 DrawThread

class SurfaceViewActivity : BaseActivity<BaseViewModel, ActivitySurfaceViewBinding>(), SurfaceHolder.Callback  {

    private lateinit var waveformSurfaceView: SurfaceView
    private var drawThread: DrawThread? = null


    override fun initView(savedInstanceState: Bundle?) {
        waveformSurfaceView = mBind.waveformSurfaceView
        val holder = waveformSurfaceView.holder
        holder.addCallback(this)
    }


    override fun surfaceCreated(holder: SurfaceHolder) {
        // 启动绘图线程
        drawThread = DrawThread(holder)
        drawThread?.start()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        // 尺寸改变时可处理,此处暂不需要
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // 停止绘图线程
        val thread = drawThread
        thread?.setRunning(false)
        thread?.join()
        drawThread = null
    }

    /**
     * 绘图线程:在 Surface 上绘制动态正弦波
     */
    private class DrawThread(private val surfaceHolder: SurfaceHolder) : Thread() {
        private var isRunning = true
        private val paint = Paint().apply {
            color = Color.CYAN
            style = Paint.Style.STROKE
            strokeWidth = 5f
        }
        private val backgroundPaint = Paint().apply {
            color = Color.DKGRAY
        }

        fun setRunning(running: Boolean) {
            isRunning = running
        }

        override fun run() {
            var time = 0L
            var canvas: Canvas? = null

            while (isRunning) {
                time++
                canvas = null
                try {
                    // 锁定画布
                    canvas = surfaceHolder.lockCanvas() ?: continue

                    val width = canvas.width
                    val height = canvas.height

                    // 绘制背景
                    canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint)

                    // 绘制正弦波
                    var prevX = 0f
                    var prevY = height / 2f

                    for (x in 0 until width) {
                        val angle = (x + time * 5) / (width / 5.0).toFloat() * Math.PI * 2
                        val y = (height / 2.0 + Math.sin(angle) * (height / 2.0 * 0.8)).toFloat()

                        canvas.drawLine(prevX, prevY, x.toFloat(), y, paint)
                        prevX = x.toFloat()
                        prevY = y
                    }

                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    if (canvas != null) {
                        try {
                            surfaceHolder.unlockCanvasAndPost(canvas)
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                }
            }
        }
    }

    /**
     * 安全地等待线程结束
     */
    private fun DrawThread?.join() {
        if (this != null && isAlive) {
            try {
                join()
            } catch (e: InterruptedException) {
                Thread.currentThread().interrupt()
            }
        }
    }

}

运行结果:

gif 看着有点卡,实际还是挺丝滑的。

二、TextureView

TextureView 的实现方式则完全不同,它将内容渲染到一个 OpenGL ES 纹理 (Texture) 中,然后这个纹理被附加到 View 的表面。

介绍

Android 4.0 以后加入,与 SurfaceView 一样继承 View,被绘制在正常的 View 层级中,可以用于实现实时预览等功能。它不是直接绘制内容,而是从一个 SurfaceTexture 中获取图像流,然后将这个图像流作为纹理渲染到自己的 View 表面上,TextureView 必须在硬件加速的窗口中才能工作。

原理:

  1. SurfaceTexture 本质是一个能接收图像流的 Buffer(生产者-消费者模型)。
  2. GPU 或 MediaCodec 可以把图像帧渲染到这个 SurfaceTexture。
  3. TextureView 把 SurfaceTexture 的内容作为纹理(Texture)绘制到 Canvas 中。
  4. 因为是 View 内部绘制,所以支持缩放、旋转、透明等。

优点:

  • 普通 View 特性:由于它是一个普通的 View,你可以像对待其他 View 一样对它进行操作。例如,可以轻松地对它进行缩放、旋转、透明度等动画,也可以放在其他 View 的上面或下面。
  • 更灵活的布局:因为它融入了 View 层级,所以更容易进行布局,可以方便地在上面叠加其他 UI 元素。
  • 截图:可以直接对它进行截图操作,这对于需要将视频画面保存为图片等场景非常有用。

缺点:

  • 性能开销:它的渲染过程会消耗更多的 GPU 资源,因为内容需要先渲染到纹理,然后再由 View 层级进行绘制。在一些性能要求极高的场景下,可能会比 SurfaceView 稍逊一筹。
  • 渲染在主线程:尽管内容源(如视频解码)可以在后台线程进行,但最终的纹理渲染和 View 的绘制过程仍然会影响到主 UI 线程。如果内容更新过于频繁,可能会对 UI 线程造成压力,导致卡顿。
  • 硬件加速依赖:它必须依赖于硬件加速,否则将无法正常工作。
  • 在5.0以前在主线程渲染,5.0以后有单独的渲染线程。

应用场景

  • 需要视频与 UI 混合的播放器(比如视频背景+透明按钮)。
  • 视频缩略图、视频特效。
  • 需要旋转/缩放/动画的视频组件。

TextureView 使用注意:

  • 没 GPU = TextureView 无法工作;
  • 即使设备有 GPU,如果关闭了硬件加速(比如在 AndroidManifest 里 android:hardwareAccelerated="false"),TextureView 也会失效
  • 官方文档明确写了:TextureView 必须依赖硬件加速才能渲染

使用示例

布局代码:

    <TextureView
        android:id="@+id/videoTextureView"
        android:layout_width="300dp"
        android:layout_height="200dp"
        android:layout_margin="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/rotateButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Rotate"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toBottomOf="@id/videoTextureView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

核心代码。使用 MediaPlayer 来播放视频,点击旋转按钮把视频旋转一圈:

class TextureViewActivity : BaseActivity<BaseViewModel, ActivityTextureViewBinding>(), TextureView.SurfaceTextureListener {

    private lateinit var videoTextureView: TextureView
    private lateinit var rotateButton: Button
    private var mediaPlayer: MediaPlayer? = null

    override fun initView(savedInstanceState: Bundle?) {
        videoTextureView = mBind.videoTextureView
        rotateButton = mBind.rotateButton

        // 设置 SurfaceTextureListener
        videoTextureView.surfaceTextureListener = this

        rotateButton.setOnClickListener {
            val anim = RotateAnimation(
                0f, 360f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f
            ).apply {
                interpolator = LinearInterpolator()
                duration = 1500
            }
            videoTextureView.startAnimation(anim)
        }
    }

    override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
        mediaPlayer = MediaPlayer().apply {
            try {
                // 设置数据源:res/raw/sea.mp4
                val uri = Uri.parse("android.resource://$packageName/${R.raw.sea}")
                setDataSource(this@TextureViewActivity, uri)

                // 设置输出到 TextureView 的 SurfaceTexture
                setSurface(Surface(surface))

                // 异步准备
                prepareAsync()

                setOnPreparedListener { mp ->
                    start() // 准备完成后自动播放
                }

                isLooping = true // 循环播放

            } catch (e: IOException) {
                e.printStackTrace()
                Toast.makeText(this@TextureViewActivity, "视频播放失败!", Toast.LENGTH_SHORT).show()
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this@TextureViewActivity, "播放器初始化失败!", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
        // 可处理视频尺寸变化,当前无需特殊处理
    }

    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
        // 释放 MediaPlayer 资源
        mediaPlayer?.let { player ->
            if (player.isPlaying) {
                player.stop()
            }
            player.release()
            mediaPlayer = null
        }
        return true // 返回 true,表示我们自行处理了销毁
    }

    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
        // 每一帧更新时调用,可用于截图或图像处理,此处留空
    }

    override fun onDestroy() {
        super.onDestroy()
        // 确保在 Activity 销毁时释放资源
        mediaPlayer?.let { player ->
            if (player.isPlaying) {
                player.stop()
            }
            player.release()
            mediaPlayer = null
        }
    }

}

运行结果:

三、总结

性能:

指标SurfaceViewTextureView
渲染延迟低(可直接 Overlay 到屏幕)高一些(需要 GPU 纹理合成)
CPU 占用低(UI 线程无压力)略高(UI 线程参与绘制提交)
GPU 占用低(Overlay 模式)或中(GPU 合成)高(所有帧都要 GPU 合成)
内存占用一般较低稍高(需要额外纹理缓冲)

选用场景:

SurfaceView:高性能、低延迟、独立线程,缺乏 UI 动画能力。

TextureView:灵活、可混合、支持动画,但性能稍低、延迟稍高。

简单来说,如果你只关心性能,不在乎 UI 效果,用 SurfaceView;如果你关心 UI 效果和灵活性,对性能要求不是那么极致,用 TextureView