SurfaceView 简介
什么是 SurfaceView
SurfaceView 是 Android 中一种比较特殊的 View,它跟平时时候的 TextView、Button 等 最大的区别是它跟它的视图容器并不是在同一个视图层上。SurfaceView 的工作方式是创建一个置于应用窗口之后的新窗口。在屏幕显示的视图层中嵌入了一块用做图像绘制的独立的 Surface 视图,它不与宿主窗口共享同一个绘图表面。相当于在屏幕上挖了个洞来显示它所绘制的图像。SurfaceView 窗口刷新的时候不需要重绘应用程序的窗口。另外,SurfaceView 的绘制也可以在一个独立的线程中完成,所以对 SurfaceView 的绘制并不会影响到主线程的运行。因此可以实现复杂而高效的UI。
为什么要使用 SurfaceView
SurfaceView 一般用来实现动态的或者比较复杂的图像还有动画的显示。
相关的几个类
- SurfaceHolder:顾名思义,就是 Surface的持有者,通过它的对象我们可以操作Surface。SurfaceView.getHolder()方法可以获得SurfaceView的SurfaceHolder对象。
- SurfaceHolder.Callback:实现 Surface生命周期的回调方法。
SurfaceView 的使用
| 1 | public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback{ | 
上面的自定义 MySurfaceView 继承自 SurfaceView,并实现 SurfaceHolder.Callback 接口。
对于实现 SurfaceHolder.Callback 接口,其实就是实现了它的几个关于 Surface 的生命周期的方法:
- surfaceCreated()
- surfaceChanged()
- surfaceDestroyed()
SurfaceView的SurfaceHolder提供了两个lockCanvas方法:
- lockCanvas():锁住整张画布,绘画完成后也更新整张画布的内容到屏幕上
- lockCanvas(Rect inOutDirty):锁住画布中的某个区域,绘画完成后也只更新这个区域的内容到屏幕。只更新必要的画面内容以节省时间,提高程序运行的效率,适用于大动态画面的场景。
通过这两个方法来获取当前的 Canvas 绘图对象。接下来就可以在画布上进行绘制操作了,就和普通的 View 上面绘制没有什么两样了。
SurfaceView 特点
多线程绘图
SurfaceView 的绘制并不会影响到主线程的运行,因此可以实现复杂而高效的UI。
| 1 | @Override | 
双缓冲
其实这里叫双缓冲也不太合适,应该叫多缓冲,因为各个手机厂商的定制Rom设置的缓冲数量并不一样。
什么是双缓冲呢?我们先通过一个例子来了解一下:
我们先通过下面代码画一个方块A:
| 1 | @Override | 
通过 lockCanvas() 获取整个画布,得到下面的结果:

再通过下面的代码画第二个方块B:
| 1 | @Override | 
得到下面的结果:

这里只出现了方块B,那么以前画的方块A,去哪里了,前面不是说 lockCanvas() 得到的画布是会保留以前的绘制内容的吗?
我们接着画方块,知道画第四个方块D的时候,方块A终于出现在画布上了:
| 1 | @Override | 

这是为什么呢?Surface 在更新画面的时候使用了缓冲机制,也就是在画布和屏幕之间建立了多个跟屏幕一样大小的内存区域,即缓冲区。
在 Surface 刚建立起来的时候,它的两个缓冲区内存中是没有任何图像的,即都是全黑。
 1.在画方块A前,我使用 lockCanvas() 接口,于是 Surface 锁住“缓冲区0”的全部区域,往画布上画方块A,被送到“缓冲区0”中。画完这1帧,使用  unlockCanvasAndPost() 接口,则会将“缓冲区0”中所有的图像都送到屏幕中进行显示,画面显示正常。
 2.在画方块B前,我使用 lockCanvas() 接口,于是 Surface 锁住“缓冲区1”的全部区域,往画布上画方块B,被送到“缓冲区1”中。画完这1帧,使用  unlockCanvasAndPost() 接口,则会将“缓冲区1”中所有的图像都送到屏幕中进行显示,画面只显示方块B。
 3….
 4.在画方块D前,我使用 lockCanvas() 接口,于是 Surface 锁住“缓冲区0”的全部区域,往画布上画方块D,和原先的方块A一起都被送到“缓冲区0”中。画完这1帧,使用  unlockCanvasAndPost() 接口,则会将“缓冲区0”中所有的图像都送到屏幕中进行显示,画面显示方块A和D。
 因此,我们在平时使用的时候就要注意,要避免上面的情况,就要保证每一帧绘制前,要把上一帧的内容都再绘制一遍。
下面来看一下下面代码的绘制效果:
| 1 | @Override | 
这里使用了 Bitmap 作为缓存来保存上一帧的数据,每一帧的数据都是在上一帧的基础上来画的。
接下来我们换一种绘制方式:
| 1 | @Override | 

这下四个方块都显示了,这是因为 lockCanvas(Rect dirty) 锁住画布中的某个区域,绘画完成后也只更新当前缓冲区的这个区域的内容到屏幕。那么这个区域以外的部分都会保留在屏幕上面。
硬件加速
我们在 surfaceCreated 方法中加断点调试 canvas.isHardwareAccelerated(); 发现返回 false,而且 canvas 对象为 Surface$CompatibleCanvas 对象。CompatibleCanvas 类是 Surface 的一个内部类,继承自 Canvas ,包含一个矩阵对象 Matrix,覆盖了 setMatrix 和 getMatrix 方法。我们知道 Canvas 是不起用硬件加速的,底层使用 Skai 来进行绘制,因此 CompatibleCanvas 也是没有使用硬件加速的。这就是为什么通过 SurfaceView.getHolder().lockCanvas() 得到的 Canvas 是软绘制的原因。
那这样来说是不是我们就无法使用硬件加速进行GPU渲染了呢?答案是否定的,我们可以通过下面两种方式使用GPU渲染:
- 自已创建 OpenGL 上下文,接入3D引擎。具体可以参考:systemui/ImageWallpaper.java,里面提供了用 OpenGL 和 Canvas 来绘制壁纸的两种方法。
- 使用 GLSurfaceView,这个后面博客会介绍。
- Android 6.0 版本的 Surface类提供了一个lockHardwareCanvas方法,用此方法可以得到硬件加速的Canvas。
下面来看一段使用 lockHardwareCanvas 进行GPU渲染的方法:
| 1 | @Override | 
通过调试发现 canvas 对象为 DisplayListCanvas 对象,它是使用GPU渲染的,后面会进行详细介绍。
和普通View的差异
无法做旋转等动画
SurfaceView 提供一个直接的绘图表面(Surface)嵌入到视图结构层次中。你可以控制这个Surface的格式,大小,SurfaceView负责在屏幕上正确的摆放Surface。简单说就是SurfaceView拥有自己的Surface,它与宿主窗口是分离的。
SurfaceView 在 7.0 以前版本是不支持平移,缩放,旋转等动画,在7.0 以后版本可以支持平移,缩放的动画操作。但无法进行旋转操作。
如图是将 Surface 旋转30°后的效果,发现绘制内容并没有跟随旋转。


视频播放器
Android 提供的组件 VideoView 是使用 SurfaceView 的。感兴趣的可以参考源码。
另外,请参考我的github的Demo:FloatWindowPlayer 中的 SurfaceView 部分,实现了悬浮窗播放器。
和 TextureView 对比
TextureView 的特点是支持旋转等动画,但是它必须在硬件加速的窗口中使用,占用内存比SurfaceView高,在5.0以前在主线程渲染,5.0以后有单独的渲染线程。
| SurfaceView | TextureView | |
|---|---|---|
| 内存 | 低 | 高 | 
| 绘制 | 及时 | 1-3帧的延迟 | 
| 耗电 | 低 | 高 | 
| 动画和截图 | 不支持 | 支持 | 
综合以上对比,那么我们的做视频播放器时应该如何选择呢?
从性能和安全性角度出发,使用播放器优先选SurfaceView。
由于 SurfaceView 有自己独立的 Window,因此 SurfaceView 也不能放到 ListView 或者 ScrollView中,因此,在列表中播放视频就无法实现了,只能选择 TextureView。
推荐文章
小窗播放视频的原理和实现(上):https://cloud.tencent.com/developer/article/1034235
小窗播放视频的原理和实现(下):https://cloud.tencent.com/developer/article/1047885