核心揭秘:GLSurfaceView 和 GLTextureView

入职转眼三年,而这三年一直围绕着 SurfaceView 或 TextureView 打交道,如今业务调整即将远离,确需要总结下。

对于要实现复杂的 2D/3D 效果,在 Android 上一般是基于 opengl es 来实现(Vulkan要7.0及以上)。那 opengl es 的绘制载体是什么呢?

普通的 View 在主线程中渲染,是很难承载复杂的、频繁刷新的绘制。此时就会选择 SurfaceView 或改版的 TextureView 在独立的子线程中实现。

一、GLSurfaceView

1、SurfaceView

SurfaceView 拥有独立的绘制表面 Surface,运行在独立的子线程中,并实现了多缓冲技术。其类图如下:

SurfaceView

Surface

Surface 是原生缓冲器(Raw Buffer)的句柄,由屏幕图像合成器(Screen Compositor)管理。可以获取 Canvas,来保存当前窗口的像素数据。

Surface 是实现多缓冲的关键,每次通过 lockCanvas 获取一个缓冲区的 Canvas 对象并锁住,绘制完后调用 unlockCanvasAndPost 提交当前缓冲区的 Canvas 到屏幕上显示。SurfaceView 使用了 Surface 的多缓冲来实现,具体是双缓冲还是三缓冲取决于系统和厂商定制。

SurfaceHolder

是一个针对 Surface 的抽象接口,用来控制 Surface 的大小、格式和监听变化。SurfaceHolder.Callback 是 SurfaceHolder 的一个内部接口,可以通过 SurfaceHolder 来添加和移除。

SurfaceView 内部持有一个 SurfaceHolder 的实现,并对 SurfaceHoler.Callback 进行管理和触发。

SurfaceView

用来显示 Surface 数据的子 View。它拥有独立的 Surface,与宿主窗口 DecorView 的 Surface 是分离的。而 Surface 会在 WindowManagerService 中有自己的 windowState,并在 SurfaceFlinger 中对应一个独立的 Layer。

surfaceView_layer

虽然在 Client 端在 View hierachy 里,但在 Server 端(WMS和SF)中,它与宿主窗口是分离的。这样就非常容易把 Surface 的渲染放到子线程去做。

SurfaceView 在 Server 端既然是个独立的 Layer,那数据是和宿主窗口一起显示的呢?SurfaceView 默认在宿主窗口的下面,是通过在宿主窗口里“挖洞”设置一块透明区域,把下边的 Layer 给显示的。

那怎么把 SurfaceView 显示在宿主窗口的上边呢?当然可以通过 setZOrderOnTop 显示在宿主窗口的上面,同时通过 getHolder().setFormat(PixelFormat.TRANSPARENT) 来设置透明,把下面的宿主窗口显示。

SurfaceView 提供了两个线程的处理:UI 线程和渲染线程。所有 SurfaceView 和 SurfaceHolder.Callback 的方法都应该在 UI 线程中执行,渲染线程访问的各种变量应该做同步处理。

2、GLSurfaceView

GLSurfaceView 在 SurfaceView 的基础上,加入了 EGL 的管理,并自带了一个 GLThread 绘制线程(EGLContext 创建 GL 环境所在线程)。

GLSurfaceView

GLSurfaceView 把 Surface 的创建、改变和每一帧渲染抽象成 Render 接口,让外部实现注入。同时通过 EglHelper 管理 EGL display、Context、Surface 和 Config。

  • 渲染模式:支持按需渲染(RENDERMODE_WHEN_DIRTY)和连续渲染(RENDERMODE_CONTINUOUSLY),连续渲染 16 ms 回调一次 onDrawFrame,按需渲染需要主动调用 requestRender 触发。
  • GPU 加速:GLSurfaceView 使用 opengl 绘制,充分利用 GPU 能力,加速提高绘制效率, 相比 SurfaceView 使用画布绘制的效率要高很多。

EGL

EGL(javax.microedition.khronos.egl ) 是定义了控制 displays,contexts 以及 surfaces 的统一的平台接口:

EGL

  • Display(EGLDisplay) :对实际显示设备的抽象。
  • Surface(EGLSurface):对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer, Stencil Buffer ,Depth Buffer。
  • Context(EGLContext):存储 OpenGL ES 绘图的一些状态信息。

渲染流程

接下来主要分析在 GLSurfaceView 的 GLThread 渲染线程中是怎么执行的,逻辑在 guardedRun 的死循环中。

1、判断 EventQueue 是否有执行的 runnable
1
2
3
4
if (! mEventQueue.isEmpty()) {
event = mEventQueue.remove(0);
break;
}

对于需要 GL 环境操作的事件,可以通过 queueEvent 放入到 eventQueue 中,在每次渲染时就会先执行队列里的任务。UI 线程和渲染线程不是同一个,禁止在 UI 线程调用 opengl es 的 API,

2、更新 UI State 状态
1
2
3
4
5
6
7
// Update the pause state.
boolean pausing = false;
if (mPaused != mRequestPaused) {
pausing = mRequestPaused;
mPaused = mRequestPaused;
sGLThreadManager.notifyAll();
}

如果当前界面发生 onResume 或 onPause 变化,则 mPaused 修改后,会改变 GLThread 的绘制状态:

1
2
3
4
5
private boolean readyToDraw() {
return (!mPaused) && mHasSurface && (!mSurfaceIsBad)
&& (mWidth > 0) && (mHeight > 0)
&& (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY));
}
3、判断是否初始化 opengl es 环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// If we don't have an EGL context, try to acquire one.
if (! mHaveEglContext) {
if (askedToReleaseEglContext) {
askedToReleaseEglContext = false;
} else {
try {
mEglHelper.start();
} catch (RuntimeException t) {
sGLThreadManager.releaseEglContextLocked(this);
throw t;
}
mHaveEglContext = true;
createEglContext = true;
sGLThreadManager.notifyAll();
}
}

若没有 EglContext,则通过 EglHelper.start 创建 opengl es 环境,获取 EGLDisplay,EGLConfig对象,创建 EGLContext 实例。

4、判断创建 Surface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (createEglSurface) {
if (mEglHelper.createSurface()) {
synchronized(sGLThreadManager) {
mFinishedCreatingEglSurface = true;
sGLThreadManager.notifyAll();
}
} else {
synchronized(sGLThreadManager) {
mFinishedCreatingEglSurface = true;
mSurfaceIsBad = true;
sGLThreadManager.notifyAll();
}
continue;
}
createEglSurface = false;
}

创建 Surface 时,要确保 EGLSurface 和 EGLContext 关联起来,将渲染环境设置到当前线程。

5、判断创建 GL 环境
1
2
3
4
5
if (createGlInterface) {
gl = (GL10) mEglHelper.createGL();
createGlInterface = false;
}

对当前的 EGL context 创建 GL 对象。

6、触发 SurfaceCreate 或 SurfaceChange 回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (createEglContext) {
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
try {
view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
createEglContext = false;
}
if (sizeChanged) {
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
try {
view.mRenderer.onSurfaceChanged(gl, w, h);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
sizeChanged = false;
}

对于创建 EGLContext 时,回调 onSurfaceCreated ,当大小改变时,回调 onSurfaceChanged

7、触发 onDrawFrame 进行渲染
1
2
3
4
5
6
7
8
9
10
{
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
try {
view.mRenderer.onDrawFrame(gl);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
}

把 GL 回调到 onDrawFrame 渲染。

8、交换缓冲区
1
int swapError = mEglHelper.swap();

把当前渲染的缓冲数据交换到当前屏幕上显示。

二、GLTextureView

GLTextureView 是仿照 GLSurfaceView,在 TextureView 的基础上添加了 EGL 的管理和 GLThread 线程绘制。而 TextureView 是基于 SurfaceTexture 实现的,下边依次介绍。

1、SurfaceTexture

SurfaceTexture 是从图像流中捕获帧数据,用作 OpenGL ES 的纹理。相比 SurfaceView,接收图像流后不需要显示出来,可用于图像流数据的二次处理(如 Camera 滤镜,桌面特效等)。

surfaceTexture

SurfaceTexture 核心逻辑都在 native。Surface 作为生产者,通过 BufferQueueProducer 向 BufferQueue 写GraphicBuffer。GLConsumer 作为消费者,通过 BufferQueueConsumer,从 BufferQueue 读取 GraphicBuffer 并转化为纹理。

GLConsumer 把 GraphicBuffer 转化为纹理后,可以通知注册 OnFrameAvailableListener 的监听者更新,也可以通知 SurfaceFlinger 合成 Layer 进行上屏。

2、TextureView

是 View 的子类,在 WMS 中没有独立的窗口,和 View hierachy 普通 View 一样管理和绘制。

TextureView

TextureView 使用硬件渲染的 DisplayListCanvas 来绘制,在重写的 draw 方法中,若不支持硬件绘制则直接返回,接着判断 SurfaceTexture 和 TextureLayer 是否存在,若不存在则创建和初始化。然后把收到的图像数据作为纹理更新到 TextureLayer 上。

TextureView 本质上控制的是 TextureLayer 纹理对象,每次在 ViewRootImpl 刷新时同步宽高,可以和普通的 view 一样做动画。SurfaceView 控制的是 Surface 对象,操作的单位不是 Texture,而是 GraphicBuffer 对象,其宽高没有和 View 的绘制同步,而是在 Surface 每次刷新后的回调设置。

3、GLTextureView

GLTextureView

TextureView 还是使用 Canvas 绘制,没有携带高效的 opengl es 绘制,也没有独立的渲染线程。因此,仿造 GLSurfaceView,对 TextureView 引入 GLThread 和 EGL 环境管理组成 GLTextureView。对外暴露 Render 创建、改变和每帧 opengl es 渲染的接口,屏蔽掉 EGL 的管理和线程的切换等。

三、比较

SurfaceView

优点

  • 可以在一个独立的线程中绘制,实现了多缓冲机制
  • 有独立的窗口管理,可以通过设置 Z 轴调整和宿主窗口的层级

缺点

  • SurfaceView 在 7.0 以下,不支持平移,缩放等动画
  • 在低端机型上比较容易 ANR,因为 UI 状态变更时都会进行锁等待

TextureView

优点

  • 支持移动、旋转、缩放等动画
  • 能作为 View hierachy 的一个普通 View 调整层级和管理

缺点

  • 必须在硬件加速的窗口中使用,占用内存比 SurfaceView 高(有内部缓冲队列)
  • 由于失效(invalidation)和缓冲的特性,TextureView 增加了额外 1~3 帧的延迟显示画面更新
知道是不会有人点的,但万一被感动了呢?