Android图形架构

4,519 阅读1小时+

Surface、SurfaceHolder、EGLSurface、SurfaceView、GLSurfaceView、SurfaceTexture、TextureView、SurfaceFlinger 和 Vulkan。

低级别组件

BufferQueue 和 gralloc。BufferQueue 将可生成图形数据缓冲区的组件(生产者)连接到接受数据以便进行显示或进一步处理的组件(消费者)。通过供应商专用 HAL 接口实现的 gralloc 内存分配器将用于执行缓冲区分配任务。

SurfaceFlinger、Hardware Composer 和虚拟显示屏。SurfaceFlinger 接受来自多个源的数据缓冲区,然后将它们进行合成并发送到显示屏。Hardware Composer HAL (HWC) 确定使用可用硬件合成缓冲区的最有效的方法,虚拟显示屏使合成输出可在系统内使用(录制屏幕或通过网络发送屏幕)。

Surface、Canvas 和 SurfaceHolder。Surface 可生成一个通常由 SurfaceFlinger 使用的缓冲区队列。当渲染到 Surface 上时,结果最终将出现在传送给消费者的缓冲区中。Canvas API 提供一种软件实现方法(支持硬件加速),用于直接在 Surface 上绘图(OpenGL ES 的低级别替代方案)。与视图有关的任何内容均涉及到 SurfaceHolder,其 API 可用于获取和设置 Surface 参数(如大小和格式)。

EGLSurface 和 OpenGL ES。OpenGL ES (GLES) 定义了用于与 EGL 结合使用的图形渲染 API。EGI 是一个规定如何通过操作系统创建和访问窗口的库(要绘制纹理多边形,请使用 GLES 调用;要将渲染放到屏幕上,请使用 EGL 调用)。此页还介绍了 ANativeWindow,它是 Java Surface 类的 C/C++ 等价类,用于通过原生代码创建 EGL 窗口表面。

Vulkan。Vulkan 是一种用于高性能 3D 图形的低开销、跨平台 API。与 OpenGL ES 一样,Vulkan 提供用于在应用中创建高质量实时图形的工具。Vulkan 的优势包括降低 CPU 开销以及支持 SPIR-V 二进制中间语言。

高级别组件

SurfaceView 和 GLSurfaceView。SurfaceView 结合了 Surface 和 View。SurfaceView 的 View 组件由 SurfaceFlinger(而不是应用)合成,从而可以通过单独的线程/进程渲染,并与应用界面渲染隔离。GLSurfaceView 提供帮助程序类来管理 EGL 上下文、线程间通信以及与“Activity 生命周期”的交互(但使用 GLES 时并不需要 GLSurfaceView)。

SurfaceTexture。SurfaceTexture 将 Surface 和 GLES 纹理相结合来创建 BufferQueue,而您的应用是 BufferQueue 的消费者。当生产者将新的缓冲区排入队列时,它会通知您的应用。您的应用会依次释放先前占有的缓冲区,从队列中获取新缓冲区并执行 EGL 调用,从而使 GLES 可将此缓冲区作为外部纹理使用。Android 7.0 增加了对安全纹理视频播放的支持,以便用户能够对受保护的视频内容进行 GPU 后处理。

TextureView。TextureView 结合了 View 和 SurfaceTexture。TextureView 对 SurfaceTexture 进行包装,并负责响应回调以及获取新的缓冲区。在绘图时,TextureView 使用最近收到的缓冲区的内容作为其数据源,根据 View 状态指示,在它应该渲染的任何位置和以它应该采用的任何渲染方式进行渲染。View 合成始终通过 GLES 来执行,这意味着内容更新可能会导致其他 View 元素重绘。

BufferQueue 和 gralloc

BufferQueue 类是 Android 中所有图形处理操作的核心。它的作用很简单:将生成图形数据缓冲区的一方(生产方)连接到接受数据以进行显示或进一步处理的一方(消耗方)。几乎所有在系统中移动图形数据缓冲区的内容都依赖于 BufferQueue。

gralloc 内存分配器会进行缓冲区分配,并通过供应商特定的 HAL 接口来实现。alloc() 函数获得预期的参数(宽度、高度、像素格式)以及一组用法标记。

BufferQueue 生产方和消耗方

基本用法很简单:生产方请求一个可用的缓冲区 (dequeueBuffer()),并指定一组特性,包括宽度、高度、像素格式和用法标记。生产方填充缓冲区并将其返回到队列 (queueBuffer())。随后,消耗方获取该缓冲区 (acquireBuffer()) 并使用该缓冲区的内容。当消耗方操作完毕后,将该缓冲区返回到队列 (releaseBuffer())。

最新的 Android 设备支持“同步框架”,这使得系统能够在与可以异步处理图形数据的硬件组件结合使用时提高工作效率。例如,生产方可以提交一系列 OpenGL ES 绘制命令,然后在渲染完成之前将输出缓冲区加入队列。该缓冲区伴有一个栅栏,当内容准备就绪时,栅栏会发出信号。当该缓冲区返回到空闲列表时,会伴有第二个栅栏,因此消耗方可以在内容仍在使用期间释放该缓冲区。该方法缩短了缓冲区通过系统时的延迟时间,并提高了吞吐量。

队列的一些特性(例如可以容纳的最大缓冲区数)由生产方和消耗方联合决定。但是,BufferQueue 负责根据需要分配缓冲区。除非特性发生变化,否则将会保留缓冲区;例如,如果生产方请求具有不同大小的缓冲区,则系统会释放旧的缓冲区,并根据需要分配新的缓冲区。

生产方和消耗方可以存在于不同的进程中。目前,消耗方始终创建和拥有数据结构。在旧版本的 Android 中,只有生产方才进行 Binder 处理(即生产方可能在远程进程中,但消耗方必须存在于创建队列的进程中)。Android 4.4 和更高版本已发展为更常规的实现。

BufferQueue 永远不会复制缓冲区内容(移动如此多的数据是非常低效的操作)。相反,缓冲区始终通过句柄进行传递。

SurfaceFlinger 和 Hardware Composer

拥有图形数据缓冲区的确不错,如果还能在设备屏幕上查看它们就更是锦上添花了。这正是 SurfaceFlinger 和 Hardware Composer HAL 的用武之地。

SurfaceFlinger 的作用是接受来自多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。当应用进入前台时,WindowManager 服务会向 SurfaceFlinger 请求一个绘图 Surface。SurfaceFlinger 会创建一个其主要组件为 BufferQueue 的层,而 SurfaceFlinger 是其消耗方。生产方端的 Binder 对象通过 WindowManager 传递到应用,然后应用可以开始直接将帧发送到 SurfaceFlinger。

大多数应用通常在屏幕上有三个层:屏幕顶部的状态栏、底部或侧面的导航栏以及应用的界面。有些应用会拥有更多或更少的层(例如,默认主屏幕应用有一个单独的壁纸层,而全屏游戏可能会隐藏状态栏)。每个层都可以单独更新。状态栏和导航栏由系统进程渲染,而应用层由应用渲染,两者之间不进行协调。

设备显示会按一定速率刷新,在手机和平板电脑上通常为每秒 60 帧。如果显示内容在刷新期间更新,则会出现撕裂现象;因此,请务必只在周期之间更新内容。在可以安全更新内容时,系统便会收到来自显示设备的信号。由于历史原因,我们将该信号称为 VSYNC 信号。

刷新率可能会随时间而变化,例如,一些移动设备的刷新率范围在 58 fps 到 62 fps 之间,具体要视当前条件而定。对于连接了 HDMI 的电视,刷新率在理论上可以下降到 24 Hz 或 48 Hz,以便与视频相匹配。由于每个刷新周期只能更新屏幕一次,因此以 200 fps 的刷新率为显示设备提交缓冲区只是在做无用功,因为大多数帧永远不会被看到。SurfaceFlinger 不会在应用提交缓冲区时执行操作,而是在显示设备准备好接收新的缓冲区时才会唤醒。

当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 总是需要可显示的内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。

SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。

Hardware Composer HAL (HWC) 是在 Android 3.0 中推出的,并且多年来一直都在不断演进。它主要是用来确定通过可用硬件来合成缓冲区的最有效方法。作为 HAL,其实现是特定于设备的,而且通常由显示设备硬件原始设备制造商 (OEM) 完成。

Surface 和 SurfaceHolder

Surface 表示通常(但并非总是!)由 SurfaceFlinger 消耗的缓冲区队列的生产方。当您渲染到 Surface 上时,产生的结果将进入相关缓冲区,该缓冲区被传递给消耗方。Surface 不仅仅是您可以随意擦写的原始内存数据块。

用于显示 Surface 的 BufferQueue 通常配置为三重缓冲;但按需分配缓冲区。因此,如果生产方足够缓慢地生成缓冲区 - 也许是以 30fps 的速度在 60fps 的显示屏上播放动画 - 队列中可能只有两个分配的缓冲区。这有助于最小化内存消耗。

画布渲染

曾经有一段时间所有渲染都是用软件完成的,您今天仍然可以这样做。低级实现由 Skia 图形库提供。如果要绘制一个矩形,您可以调用库,然后它会在缓冲区中适当地设置字节。为了确保两个客户端不会同时更新某个缓冲区,或者在该缓冲区正在被显示时写入该缓冲区,您必须锁定该缓冲区才能进行访问。lockCanvas() 可锁定该缓冲区并返回用于绘制的 Canvas,unlockCanvasAndPost() 则解锁该缓冲区并将其发送到合成器。

随着时间的推移,出现了具有通用 3D 引擎的设备,于是 Android 围绕 OpenGL ES 进行了重新定位。然而,必须确保旧 API 依然适用于应用和应用框架代码,所以我们努力对 Canvas API 进行了硬件加速。从硬件加速页面的图表可以看出,整个过程并非一帆风顺。特别要注意的一点是,虽然提供给 View 的 onDraw() 方法的 Canvas 可能已硬件加速,但是当应用通过 lockCanvas() 直接锁定 Surface 时所获得的 Canvas 从未获得硬件加速。

当您锁定一个 Surface 以便访问 Canvas 时,“CPU 渲染器”将连接到 BufferQueue 的生产方,直到 Surface 被销毁时才会断开连接。大多数其他生产方(如 GLES)可以断开连接并重新连接到 Surface,但是基于 Canvas 的“CPU 渲染器”则不能。这意味着如果您已经为某个 Canvas 将相关 Surface 锁定,则无法使用 GLES 在该 Surface 上进行绘制或从视频解码器向其发送帧。

生产方首次从 BufferQueue 请求缓冲区时,缓冲区将被分配并初始化为零。有必要进行初始化,以避免意外地在进程之间共享数据。然而,当您重新使用缓冲区时,以前的内容仍然存在。如果您反复调用 lockCanvas() 和 unlockCanvasAndPost() 而不绘制任何内容,则会在先前渲染的帧之间循环。

Surface 锁定/解锁代码会保留对先前渲染的缓冲区的引用。如果在锁定 Surface 时指定了脏区域,那么它将从以前的缓冲区复制非脏像素。缓冲区很有可能由 SurfaceFlinger 或 HWC 处理;但是由于我们只需从中读取内容,所以无需等待独占访问。

应用直接在 Surface 上进行绘制的主要非 Canvas 方法是通过 OpenGL ES。

SurfaceHolder

与 Surface 配合使用的一些功能需要 SurfaceHolder,特别是 SurfaceView。最初的想法是,Surface 代表合成器管理的原始缓冲区,而 SurfaceHolder 由应用管理,并跟踪更高层次的信息(如维度和格式)。Java 语言定义对应的是底层本机实现。可以说,这种划分方式已不再有用,但它长期以来一直是公共 API 的一部分。

一般来说,与 View 相关的任何内容都涉及到 SurfaceHolder。一些其他 API(如 MediaCodec)将在 Surface 本身上运行。您可以轻松地从 SurfaceHolder 获取 Surface,因此当您拥有 SurfaceHolder 时,使用它即可。

用于获取和设置 Surface 参数(例如大小和格式)的 API 是通过 SurfaceHolder 实现的。

EGLSurface 和 OpenGL ES

OpenGL ES 定义了一个渲染图形的 API,但没有定义窗口系统。为了让 GLES 能够适合各种平台,GLES 将与知道如何通过操作系统创建和访问窗口的库结合使用。用于 Android 的库称为 EGL。如果要绘制纹理多边形,应使用 GLES 调用;如果要在屏幕上进行渲染,应使用 EGL 调用。

在使用 GLES 进行任何操作之前,需要创建一个 GL 上下文。在 EGL 中,这意味着要创建一个 EGLContext 和一个 EGLSurface。GLES 操作适用于当前上下文,该上下文通过线程局部存储访问,而不是作为参数进行传递。这意味着您必须注意渲染代码在哪个线程上执行,以及该线程上的当前上下文。

EGLSurface 可以是由 EGL 分配的离屏缓冲区(称为“pbuffer”),或由操作系统分配的窗口。EGL 窗口 Surface 通过 eglCreateWindowSurface() 调用被创建。该调用将“窗口对象”作为参数,在 Android 上,该对象可以是 SurfaceView、SurfaceTexture、SurfaceHolder 或 Surface,所有这些对象下面都有一个 BufferQueue。当您进行此调用时,EGL 将创建一个新的 EGLSurface 对象,并将其连接到窗口对象的 BufferQueue 的生产方接口。此后,渲染到该 EGLSurface 会导致一个缓冲区离开队列、进行渲染,然后排队等待消耗方使用。(术语“窗口”表示预期用途,但请注意,输出内容不一定会显示在显示屏上。)

EGL 不提供锁定/解锁调用,而是由您发出绘制命令,然后调用 eglSwapBuffers() 来提交当前帧。方法名称来自传统的前后缓冲区交换,但实际实现可能会有很大的不同。

一个 Surface 一次只能与一个 EGLSurface 关联(您只能将一个生产方连接到一个 BufferQueue),但是如果您销毁该 EGLSurface,它将与该 BufferQueue 断开连接,并允许其他内容连接到该 BufferQueue。

通过更改“当前”EGLSurface,指定线程可在多个 EGLSurface 之间进行切换。一个 EGLSurface 一次只能在一个线程上处于当前状态。

关于 EGLSurface 最常见的一个错误理解就是假设它只是 Surface 的另一方面(如 SurfaceHolder)。它是一个相关但独立的概念。您可以在没有 Surface 作为支持的 EGLSurface 上绘制,也可以在没有 EGL 的情况下使用 Surface。EGLSurface 仅为 GLES 提供一个绘制的地方。

ANativeWindow

公开的 Surface 类以 Java 编程语言实现。C/C++ 中的同等项是 ANATIONWindow 类,由 Android NDK 半公开。您可以使用 ANativeWindow_fromSurface() 调用从 Surface 获取 ANativeWindow。就像它的 Java 语言同等项一样,您可以对 ANativeWindow 进行锁定、在软件中进行渲染,以及解锁并发布。

要从原生代码创建 EGL 窗口 Surface,可将 EGLNativeWindowType 的实例传递到 eglCreateWindowSurface()。EGLNativeWindowType 是 ANativeWindow 的同义词,您可以自由地在它们之间转换。

基本的“原生窗口”类型只是封装 BufferQueue 的生产方,这一点并不足为奇。

Vulkan

Android 7.0 添加了对 Vulkan 的支持。Vulkan 是用于高性能 3D 图形的低开销、跨平台 API。与 OpenGL ES 一样,Vulkan 提供多种用于在应用中创建高质量的实时图形的工具。Vulkan 的优势包括降低 CPU 开销以及支持 SPIR-V 二进制中间语言。

应用开发者可以利用 Vulkan 来创建在 GPU 上执行命令的应用,大幅降低开销。此外,Vulkan 还可以更直接地映射到当前图形硬件中的功能,最大限度地降低驱动程序的出错概率,并减少开发者的测试时间(例如,排查 Vulkan 错误所需的时间更短)。

Vulkan 组件

Vulkan 支持包含以下组件:

Vulkan 验证层(在 Android NDK 中提供)。这是开发者在开发 Vulkan 应用期间使用的一组库。图形供应商提供的 Vulkan 运行时库和 Vulkan 驱动程序不包含使 Vulkan 运行时保持高效的运行时错误检查功能,而是使用验证库(仅在开发过程中)来查找应用在使用 Vulkan API 时出现的错误。Vulkan 验证库在开发过程中关联到应用并执行此错误检查。在找出所有 API 使用问题之后,该应用将不再需要包含这些库。

Vulkan 运行时(由 Android 提供)。这是一个原生库 ((libvulkan.so),提供称为 Vulkan 的新公共原生 API。大多数功能由 GPU 供应商提供的驱动程序实现;运行时会封装驱动程序、提供 API 拦截功能(针对调试和其他开发者工具)以及管理驱动程序与平台依赖项(如 BufferQueue)之间的交互。

Vulkan 驱动程序(由 SoC 提供)。将 Vulkan API 映射到特定于硬件的 GPU 命令以及与内核图形驱动程序的交互。

SurfaceView 和 GLSurfaceView

Android 应用框架界面是以使用 View 开头的对象层次结构为基础。所有界面元素都会经过一个复杂的测量和布局过程,该过程会将这些元素融入到矩形区域中,并且所有可见 View 对象都会渲染到一个由 SurfaceFlinger 创建的 Surface(在应用置于前台时,由 WindowManager 进行设置)。应用的界面线程会执行布局并渲染到单个缓冲区(不考虑 Layout 和 View 的数量以及 View 是否已经过硬件加速)。

SurfaceView从Android 1.0(API level 1)时就有 。它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface。我们知道,一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。相应地,在SurfaceFlinger(SF)中对应的Layer。而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SurfaceFlinger中也会有自己的Layer。如下图所示:

也就是说,虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。

SurfaceView 采用与其他视图相同的参数,因此您可以为 SurfaceView 设置位置和大小,并在其周围填充其他元素。但是,当需要渲染时,内容会变得完全透明;SurfaceView 的 View 部分只是一个透明的占位符。

当 SurfaceView 的 View 组件即将变得可见时,框架会要求 WindowManager 命令 SurfaceFlinger 创建一个新的 Surface。(这个过程并非同步发生,因此您应该提供回调,以便在 Surface 创建完毕后收到通知。)默认情况下,新的 Surface 将放置在应用界面 Surface 的后面,但可以替换默认的 Z 排序,将 Surface 放在顶层。

渲染到该 Surface 上的内容将会由 SurfaceFlinger(而非应用)进行合成。这是 SurfaceView 的真正强大之处:您获得的 Surface 可以由单独的线程或单独的进程进行渲染,并与应用界面执行的任何渲染隔离开,而缓冲区可直接转至 SurfaceFlinger。

您不能完全忽略界面线程,因为您仍然需要与 Activity 生命周期相协调,并且如果 View 的大小或位置发生变化,您可能需要调整某些内容,但是您可以拥有整个 Surface。与应用界面和其他图层的混合由 Hardware Composer 处理。

新的 Surface 是 BufferQueue 的生产者端,其消费者是 SurfaceFlinger 层。您可以使用任意提供 BufferQueue 的机制(例如,提供 Surface 的 Canvas 函数)来更新 Surface,附加 EGLSurface 并使用 GLES 进行绘制,或者配置 MediaCodec 视频解码器以便于写入。

SurfaceView可以自定义子类, 继承于SurfaceView就可以了. 使用的时候可以直接new 一个不带参数的构造函数的对象, 或者随意定义. 比如这样:

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {

在Activity中直接使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    int type = getIntent().getIntExtra("type", 0);
    if (type == 0) {
        setContentView(new MySurfaceView(this));

也可以在xml中定义, 那么自定义类里面就必须实现三个参数的那个构造方法.

自定义SurfaceView中如果要动态的更新显示, 就得启动一个线程来做. 线程启动的最好的时机就是在surfaceCreated()回调方法中. 在这个线程中画图时, 通过SurfaceHolder来获取Canvas对象, 而且用完之后必须unlock. canvas = surfaceHolder.lockCanvas(); //获取Canvas对象 surfaceHolder.unlockCanvasAndPost(canvas); // 释放Canvas对象. SurfaceView也是直接继承自View的, 也有onTouchEvent等方法, 所以除了触摸事件之类的不是问题 onMeasure(), onLayout(), onSizeChanged(), onWindowVisibilityChanged(), onAttachedToWindow() 这些方法都会被回调, 而且都是在UI线程. 但是 onDraw(),onFinishInflate()方法都 不会被回调, 这个需要注意!

合成与硬件缩放

我们来仔细研究一下 dumpsys SurfaceFlinger。当在 Nexus 5 上,以纵向方向在 Grafika 的“播放视频 (SurfaceView)”活动中播放电影时,采用以下输出;视频是 QVGA (320x240):

    type    |          source crop              |           frame           name
------------+-----------------------------------+--------------------------------
        HWC | [    0.0,    0.0,  320.0,  240.0] | [   48,  411, 1032, 1149] SurfaceView
        HWC | [    0.0,   75.0, 1080.0, 1776.0] | [    0,   75, 1080, 1776] com.android.grafika/com.android.grafika.PlayMovieSurfaceActivity
        HWC | [    0.0,    0.0, 1080.0,   75.0] | [    0,    0, 1080,   75] StatusBar
        HWC | [    0.0,    0.0, 1080.0,  144.0] | [    0, 1776, 1080, 1920] NavigationBar
  FB TARGET | [    0.0,    0.0, 1080.0, 1920.0] | [    0,    0, 1080, 1920] HWC_FRAMEBUFFER_TARGET
  • 列表顺序是从后到前:SurfaceView 的 Surface 位于后面,应用界面层位于其上,其次是处于最前方的状态栏和导航栏。
  • 源剪裁值表示 Surface 缓冲区中 SurfaceFlinger 将显示的部分。应用界面会获得一个与显示屏的完整尺寸 (1080x1920) 一样大的 Surface,但是由于渲染和合成将被状态栏和导航栏遮挡的像素毫无意义,因此将源剪裁为一个矩形(上自离顶部 75 个像素,下至离底部 144 个像素)。状态栏和导航栏的 Surface 较小,并且源剪裁描述了一个矩形(起点位于左上角 (0,0) 并且会横跨其内容)。
  • 框架值指定在显示屏上显示像素的矩形。对于应用界面层,框架会与源剪裁匹配,因为我们会将与显示屏同样大小的图层的一部分复制(或叠加)到另一个与显示屏同样大小的图层中的相同位置。对于状态栏和导航栏,两者的框架矩形大小相同,但是位置经过调整,所以导航栏出现在屏幕底部。
  • SurfaceView 层容纳我们的视频内容。源剪裁与视频的大小相匹配,而 SurfaceFlinger 了解该信息,因为 MediaCodec 解码器(缓冲区生成器)正在将同样大小的缓冲区移出队列。框架矩形具有完全不同的尺寸:984x738。

SurfaceFlinger 通过缩放(根据需要放大或缩小)缓冲区内容来填充框架矩形,以处理大小差异。之所以选择这种特定尺寸,是因为它具有与视频相同的宽高比 (4:3),并且由于 View 布局的限制(为了美观,在屏幕边缘处留有一定的内边距),因此应尽可能地宽。

如果您在同一 Surface 上开始播放不同的视频,底层 BufferQueue 会将缓冲区自动重新分配为新的大小,而 SurfaceFlinger 将调整源剪裁。如果新视频的宽高比不同,则应用需要强制重新布局 View 才能与之匹配,这将导致 WindowManager 通知 SurfaceFlinger 更新框架矩形。

如果您通过其他方式(如 GLES)在 Surface 上进行渲染,则可以使用 SurfaceHolder#setFixedSize() 调用设置 Surface 尺寸。例如,您可以将游戏配置为始终采用 1280x720 的分辨率进行渲染,这将大大减少填充 2560x1440 平板电脑或 4K 电视机屏幕所需处理的像素数。显示处理器会处理缩放。如果您不希望给游戏加上水平或垂直黑边,您可以通过设置尺寸来调整游戏的宽高比,使窄尺寸为 720 像素,但长尺寸设置为维持物理显示屏的宽高比(例如,设置为 1152x720 来匹配 2560x1600 的显示屏)。有关此方法的示例,请参阅 Grafika 的“硬件缩放练习程序”活动。

GLSurfaceView

GLSurfaceView 类提供帮助程序类,用于管理 EGL 上下文、线程间通信以及与 Activity 生命周期的交互。这就是其功能。您无需使用 GLSurfaceView 来应用 GLES。

例如,GLSurfaceView 创建一个渲染线程,并配置 EGL 上下文。当活动暂停时,状态将自动清除。大多数应用都不需要知道 EGL,便可通过 GESurfaceView 使用 GLES。

GLSurfaceView从Android 1.5(API level 3)开始加入,作为SurfaceView的补充。它可以看作是SurfaceView的一种典型使用模式。在SurfaceView的基础上,它加入了EGL的管理,并自带了渲染线程。另外它定义了用户需要实现的Render接口,提供了用Strategy pattern更改具体Render行为的灵活性。作为GLSurfaceView的Client,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView即可。如:

public class TriangleActivity extends Activity {  
    protected void onCreate(Bundle savedInstanceState) {  
        mGLView = new GLSurfaceView(this);  
        mGLView.setRenderer(new RendererImpl(this));

其中SurfaceView中的SurfaceHolder主要是提供了一些操作Surface的接口。GLSurfaceView中的EglHelper和GLThread分别实现了上面提到的管理EGL环境和渲染线程的工作。GLSurfaceView的使用者需要实现Renderer接口。

SurfaceView 和 Activity 生命周期

当使用 SurfaceView 时,使用主界面线程之外的线程渲染 Surface 是很好的做法。不过,这样就会产生一些与线程和 Activity 生命周期之间的交互相关的问题。

对于具有 SurfaceView 的 Activity,存在两个单独但相互依赖的状态机:

  1. 状态为 onCreate/onResume/onPause 的应用
  2. 已创建/更改/销毁的 Surface

当 Activity 开始时,将按以下顺序获得回调:

  • onCreate
  • onResume
  • surfaceCreated
  • surfaceChanged

如果返回,您将得到:

  • onPause
  • surfaceDestroyed(在 Surface 消失前调用)

如果旋转屏幕,Activity 将被消解并重新创建,而您将获得整个周期。您可以通过检查 isFinishing() 告知屏幕快速重新启动。启动/停止 Activity 可能非常快速,从而可能导致 surfaceCreated() 实际上是在 onPause() 之后发生。

如果您点按电源按钮锁定屏幕,则只会得到 onPause()(没有 surfaceDestroyed())。Surface 仍处于活跃状态,并且渲染可以继续。如果您继续请求,甚至可以持续获得 Choreographer 事件。如果您使用强制变向的锁屏,则当设备未锁定时,您的 Activity 可能会重新启动;但如果没有,您可以使用与之前相同的 Surface 脱离屏幕锁定。

当使用具有 SurfaceView 的单独渲染器线程时,会引发一个基本问题:线程寿命是否依赖 Surface 或 Activity 的寿命?答案取决于锁屏时您想要看到的情况:(1) 在 Activity 启动/停止时启动/停止线程,或 (2) 在 Surface 创建/销毁时启动/停止线程。

选项 1 与应用生命周期交互良好。我们在 onResume() 中启动渲染器线程,并在 onPause() 中将其停止。当创建和配置线程时,会显得有点奇怪,因为有时 Surface 已经存在,有时不存在(例如,在使用电源按钮切换屏幕后,它仍然存在)。我们必须先等待 Surface 完成创建,然后再在线程中进行一些初始化操作,但是我们不能简单地在 surfaceCreated() 回调中进行操作,因为如果未重新创建 Surface,将不会再次触发。因此,我们需要查询或缓存 Surface 状态,并将其转发到渲染器线程。

选项 2 非常具有吸引力,因为 Surface 和渲染器在逻辑上互相交织。我们在创建 Surface 后启动线程,避免了一些线程间通信问题,也可轻松转发 Surface 已创建/更改的消息。当屏幕锁定时,我们需要确保渲染停止,并在未锁定时恢复渲染;要实现这一点,可能只需告知 Choreographer 停止调用框架绘图回调。当且仅当渲染器线程正在运行时,我们的 onResume() 才需要恢复回调。尽管如此,如果我们根据框架之间的已播放时长进行动画绘制,我们可能发现,在下一个事件到来前存在很大的差距;应使用一个明确的暂停/恢复消息。

这两个选项主要关注如何配置渲染器线程以及线程是否正在执行。一个相关问题是,终止 Activity 时(在 onPause() 或 onSaveInstanceState() 中)从线程中提取状态;在此情况下,选项 1 最有效,因为在渲染器线程加入后,不需要使用同步基元就可以访问其状态。

SurfaceTexture

SurfaceTexture 类是在 Android 3.0 中推出的。就像 SurfaceView 是 Surface 和 View 的组合一样,SurfaceTexture 是 Surface 和 GLES 纹理的粗略组合(包含几个注意事项)。

和SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。

当您创建 SurfaceTexture 时,会创建一个应用是其消耗方的 BufferQueue。如果生产方将新的缓冲区加入队列,您的应用便会通过回调 (onFrameAvailable()) 获得通知。应用调用 updateTexImage()(这会释放先前保留的缓冲区),从队列中获取新的缓冲区,然后发出一些 EGL 调用,让缓冲区可作为外部纹理供 GLES 使用。

首先,SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。从下面的类图中可以看出,它核心管理着一个BufferQueue的Consumer和Producer两端。Producer端用于内容流的源输出数据,Consumer端用于获取GraphicBuffer并生成纹理。SurfaceTexture.OnFrameAvailableListener用于让SurfaceTexture的使用者知道有新数据到来。JNISurfaceTextureContext是OnFrameAvailableListener从Native到Java的JNI跳板。其中SurfaceTexture中的attachToGLContext()和detachToGLContext()可以让多个GL context共享同一个内容源。

外部纹理 (GL_TEXTURE_EXTERNAL_OES) 与 GLES (GL_TEXTURE_2D) 创建的纹理并不完全相同:您对渲染器的配置必须有所不同,而且有一些操作是不能对外部纹理执行的。关键是,您可以直接从 BufferQueue 接收到的数据中渲染纹理多边形。gralloc 支持各种格式,因此我们需要保证缓冲区中数据的格式是 GLES 可以识别的格式。为此,当 SurfaceTexture 创建 BufferQueue 时,它将消耗方用法标记设置为 GRALLOC_USAGE_HW_TEXTURE,确保由 gralloc 创建的缓冲区均可供 GLES 使用。

由于 SurfaceTexture 会与 EGL 上下文交互,因此您必须小心地从正确的会话中调用其方法。

事实证明,BufferQueue 不只是向消耗方传递缓冲区句柄。每个缓冲区都附有时间戳和转换参数。

提供转换是为了提高效率。在某些情况下,源数据可能以错误的方向传递给消耗方;但是,我们可以按照数据的当前方向发送数据并使用转换对其进行更正,而不是在发送数据之前对其进行旋转。在使用数据时,转换矩阵可以与其他转换合并,从而最大限度降低开销。

时间戳对于某些缓冲区来源非常有用。例如,假设您将生产方接口连接到相机的输出端(使用 setPreviewTexture())。要创建视频,您需要为每个帧设置演示时间戳;不过您需要根据截取帧的时间(而不是应用收到缓冲区的时间)来设置该时间戳。随缓冲区提供的时间戳由相机代码设置,从而获得一系列更一致的时间戳。

Android 5.0中将BufferQueue的核心部分分离出来,放在BufferQueueCore这个类中。BufferQueueProducer和BufferQueueConsumer分别是它的生产者和消费者实现基类(分别实现了IGraphicBufferProducer和IGraphicBufferConsumer接口)。它们都是由BufferQueue的静态函数createBufferQueue()来创建的。Surface是生产者端的实现类,提供dequeueBuffer/queueBuffer等硬件渲染接口,和lockCanvas/unlockCanvasAndPost等软件渲染接口,使内容流的源可以往BufferQueue中填graphic buffer。GLConsumer继承自ConsumerBase,是消费者端的实现类。它在基类的基础上添加了GL相关的操作,如将graphic buffer中的内容转为GL纹理等操作。到此,以SurfaceTexture为中心的一个pipeline大体是这样的:

SurfaceTexture 和 Surface

如果仔细观察 API,您会发现应用只能通过一种方式来创建简单 Surface,即通过将 SurfaceTexture 作为唯一参数的构造函数来创建。(在 API 11 之前,根本没有用于 Surface 的公开构造函数。)如果将 SurfaceTexture 视为 Surface 和纹理的组合,这看起来可能有点落后。

深入来看,SurfaceTexture 称为 GLConsumer,它更准确地反映了其作为 BufferQueue 的所有方和消耗方的角色。从 SurfaceTexture 创建 Surface 时,您所做的是创建一个表示 SurfaceTexture 的 BufferQueue 生产方端的对象。

相机可以提供一个适合作为电影进行录制的帧流。要在屏幕上显示它,您可以创建一个 SurfaceView,将 Surface 传递给 setPreviewDisplay(),然后让生产方(相机)和消耗方 (SurfaceFlinger) 完成所有工作。要录制视频,您可以使用 MediaCodec 的 createInputSurface() 创建一个 Surface,将其传递给相机,然后便可高枕无忧了。若要边显示边录制,您必须使用更多内容。

在录制视频时,连续拍摄 Activity 会显示相机录制的视频。在这种情况下,已编码的视频将写入内存中的环形缓冲区,该缓冲区可随时保存到磁盘。实现起来非常简单,只要您跟踪所有内容所在的位置即可。

该流程涉及三个 BufferQueue,分别是由应用、SurfaceFlinger 和 mediaserver 所创建:

  • 应用。该应用使用 SurfaceTexture 从相机接收帧,并将其转换为外部 GLES 纹理。
  • SurfaceFlinger。该应用声明一个我们用来显示帧的 SurfaceView。
  • MediaServer。您可以使用输入 Surface 配置 MediaCodec 编码器,以创建视频。

图 1.Grafika 的连续拍摄 Activity。箭头指示相机的数据传输路径,BufferQueue 则用颜色标示(生产方为青色,消耗方为绿色)。

已编码的 H.264 视频在应用进程中进入 RAM 中的环形缓冲区,并在点击拍摄按钮时使用 MediaMuxer 类写入到磁盘上的 MP4 文件中。

三个 BufferQueue 都在应用中通过单个 EGL 上下文处理,且 GLES 操作在 UI 线程上执行。通常,不鼓励在 UI 线程上执行 SurfaceView 渲染,但是由于我们执行的是由 GLES 驱动程序异步处理的简单操作,因此没有问题。(如果视频编码器锁定,并且我们阻止尝试将缓冲区移出队列,该应用便会停止响应。而此时,我们无论怎样操作都可能会失败。)对已编码数据的处理(管理环形缓冲区并将其写入磁盘)是在单独的线程中执行的。

大部分配置是在 SurfaceView 的 surfaceCreated() 回调中进行的。系统会创建 EGLContext,并为显示设备和视频编码器创建 EGLSurface。当新的帧到达时,我们会告知 SurfaceTexture 去获取它,并将其作为 GLES 纹理进行提供,然后使用 GLES 命令在每个 EGLSurface 上渲染它(从 SurfaceTexture 转发转换和时间戳)。编码器线程从 MediaCodec 拉取编码的输出内容,并将其存储在内存中。

Android 7.0 支持对受保护的视频内容进行 GPU 后处理。这允许将 GPU 用于复杂的非线性视频效果(例如扭曲),将受保护的视频内容映射到纹理,以用于常规图形场景(例如,使用 OpenGL ES)和虚拟现实 (VR)。

图 2.安全纹理视频播放

使用以下两个扩展程序实现支持:

  • EGL 扩展程序 (EGL_EXT_protected_content)。允许创建受保护的 GL 上下文和 Surface,它们都可以对受保护的内容进行操作。
  • GLES 扩展程序 (GL_EXT_protected_textures)。允许将纹理标记为受保护,以便它们可以用作帧缓冲区纹理附件。

Android 7.0 还会更新 SurfaceTexture 和 ACodec (libstagefright.so),这样一来,即使窗口 Surface 没有加入窗口编写器(例如 SurfaceFlinger)的队列,也允许发送受保护的内容,并且提供受保护的视频 Surface 以在受保护的上下文中使用。该操作通过在受保护的上下文(由 ACodec 验证)中创建的 Surface 上设置正确的受保护消耗方位 (GRALLOC_USAGE_PROTECTED) 来完成。

TextureView

应用程序的视频或者opengl内容往往是显示在一个特别的UI控件中:SurfaceView。SurfaceView的工作方式是创建一个置于应用窗口之后的新窗口。这种方式的效率非常高,因为SurfaceView窗口刷新的时候不需要重绘应用程序的窗口(android普通窗口的视图绘制机制是一层一层的,任何一个子元素或者是局部的刷新都会导致整个视图结构全部重绘一次,因此效率非常低下,不过满足普通应用界面的需求还是绰绰有余),但是SurfaceView也有一些非常不便的限制。

因为SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha()。

TextureView在4.0(API level 14)中引入。如果你想显示一段在线视频或者任意的数据流比如视频或者OpenGL 场景,你可以用android中的TextureView做到。它可以将内容流直接投影到View中,可以用于实现Live preview等功能。和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。值得注意的是TextureView必须在硬件加速的窗口中。它显示的内容流数据可以来自App进程或是远端进程。从类图中可以看到,TextureView继承自View,它与其它的View一样在View hierachy中管理与绘制。TextureView重载了draw()方法,其中主要把SurfaceTexture中收到的图像数据作为纹理更新到对应的HardwareLayer中。SurfaceTexture.OnFrameAvailableListener用于通知TextureView内容流有新图像到来。SurfaceTextureListener接口用于让TextureView的使用者比如MediaPlayer?知道SurfaceTexture已准备好,这样就可以把SurfaceTexture交给相应的内容源。Surface为BufferQueue的Producer接口实现类,使生产者可以通过它的软件或硬件渲染接口为SurfaceTexture内部的BufferQueue提供graphic buffer。

使用 GLES 呈现

我们已经知道,SurfaceTexture 是一个“GL 消费者”,它会占用图形数据的缓冲区,并将它们作为纹理进行提供。TextureView 会对 SurfaceTexture 进行封装,并接管对回调做出响应以及获取新缓冲区的责任。新缓冲区的就位会导致 TextureView 发出 View 失效请求。当被要求进行绘图时,TextureView 会使用最近收到的缓冲区的内容作为数据源,并根据 View 状态的指示,以相应的方式在相应的位置进行呈现。

您可以使用 GLES 在 TextureView 上呈现内容,就像在 SurfaceView 上一样。只需将 SurfaceTexture 传递到 EGL 窗口创建调用即可。不过,这样做会导致潜在问题。

在我们看到的大部分内容中,BufferQueue 是在不同进程之间传递缓冲区。当使用 GLES 呈现到 TextureView 时,生产者和消费者处于同一进程中,它们甚至可能会在单个线程上得到处理。假设我们以快速连续的方式从界面线程提交多个缓冲区。EGL 缓冲区交换调用需要使一个缓冲区从 BufferQueue 出列,而在有可用的缓冲区之前,它将处于暂停状态。只有当消费者获取一个缓冲区用于呈现时才会有可用的缓冲区,但是这一过程也会发生在界面线程上…因此我们陷入了困境。

解决方案是让 BufferQueue 确保始终有一个可用的缓冲区能够出列,以使缓冲区交换始终不会暂停。要保证能够实现这一点,一种方法是让 BufferQueue 在新缓冲区加入队列时舍弃之前加入队列的缓冲区的内容,并对最小缓冲区计数和最大获取缓冲区计数施加限制(如果您的队列有三个缓冲区,而所有这三个缓冲区均被消费者获取,那么就没有可以出列的缓冲区,缓冲区交换调用必然会暂停或失败。因此我们需要防止消费者一次获取两个以上的缓冲区)。丢弃缓冲区通常是不可取的,因此仅允许在特定情况下发生,例如生产者和消费者处于同一进程中时。

使用

唯一要做的就是获取用于渲染内容的SurfaceTexture。具体做法是先创建TextureView对象,然后实现SurfaceTextureListener接口,代码如下:

private TextureView myTexture;

public class MainActivity extends Activity implements SurfaceTextureListener{

    protected void onCreate(Bundle savedInstanceState) {
       myTexture = new TextureView(this);
       myTexture.setSurfaceTextureListener(this);
       setContentView(myTexture);
    }
}

Activity implements了SurfaceTextureListener接口因此activity中需要重写如下方法:

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    //当TextureView初始化时调用,事实上当你的程序退到后台它会被销毁,你再次打开程序的时候它会被重新初始化
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    //当TextureView的大小改变时调用
}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    //当TextureView被销毁时调用
    return true;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    //当TextureView更新时调用,也就是当我们调用unlockCanvasAndPost方法时
}

TextureView可以使用setAlpha和setRotation方法达到改变透明度和旋转的效果。

myTexture.setAlpha(1.0f);
myTexture.setRotation(90.0f);

将图片绘制到TextureView

第一步:读取图片到内存中

//从本地读取图片,这里的path必须是绝对地址
private Bitmap readBitmap(String path) throws IOException {
    return BitmapFactory.decodeFile(path);
}
//从Assets读取图片
private Bitmap readBitmap(String path) throws IOException {
    return BitmapFactory.decodeStream(getResources().getAssets().open(path));
}

第二步:将内存中的图片画到画布上,这里在画完之后需要释放Bitmap

//将图片画到画布上,图片将被以宽度为比例画上去
private void drawBitmap(Bitmap bitmap) {
    Canvas canvas = lockCanvas(new Rect(0, 0, getWidth(), getHeight()));//锁定画布
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
    Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
    Rect dst = new Rect(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
    canvas.drawBitmap(bitmap, src, dst, mPaint);//将bitmap画到画布上
    unlockCanvasAndPost(canvas);//解锁画布同时提交
}

很明显TextureView比起正常的View的优势就是可以在异步将图片画到画布上,我们可以创建一个异步线程,然后通过SystemClock.sleep()这个函数在每画完一帧都暂停一定时间,这样就实现了一个完整的过程。

private class PlayThread extends Thread {
        @Override
        public void run() {
            try {
                while (mPlayFrame < mFrameCount) {//如果还没有播放完所有帧
                    Bitmap bitmap = readBitmap(mPaths[mPlayFrame]);
                    drawBitmap(bitmap);
                    recycleBitmap(bitmap);
                    mPlayFrame++;
                    SystemClock.sleep(mDelayTime);//暂停间隔时间
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

但是,sleep函数并不能按照指定的秒数进行暂停。往往导致比需要的秒数更多。所以可以采用handler来进行播放速度的控制。

如果消息队列里目前没有合适的消息可以摘取,那么不能让它所属的线程“傻转”,而应该使之阻塞。


队列里的消息应该按其“到时”的顺序进行排列,最先到时的消息会放在队头,也就是mMessages域所指向的消息,其后的消息依次排开。

阻塞的时间最好能精确一点儿,所以如果暂时没有合适的消息节点可摘时,要考虑链表首个消息节点将在什么时候到时,所以这个消息节点距离当前时刻的时间差,就是我们要阻塞的时长。

有时候外界希望队列能在即将进入阻塞状态之前做一些动作,这些动作可以称为idle动作,我们需要兼顾处理这些idle动作。一个典型的例子是外界希望队列在进入阻塞之前做一次垃圾收集。
创建一个Thread。

初始化Looper,这里可以直接继承HandlerThread。

创建一个Handler。

通过SystemClock.uptimeMillis()取得时间,然后向Handler发送消息。

接收到消息后判断是否结束,如果未结束则将当前的时间加上间隔时间(比如40ms)后继续发送消息,不断进行循环过程。
//开启线程
mScheduler = new Scheduler(duration, paths.length, new FrameUpdateListener());
mScheduler.start();

private class FrameUpdateListener implements OnFrameUpdateListener {
    @Override
    public void onFrameUpdate(long frameIndex) {
        try {
            Bitmap bitmap = readBitmap(mPaths[(int) frameIndex]);
            drawBitmap(bitmap);
            recycleBitmap(bitmap);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们需要新建一个线程将readBitmap()移到新线程中执行,然后通过一个缓存数组(多线程之间需要加锁)进行交互。

private List<Bitmap> mCacheBitmaps;//缓存帧集合
private int mReadFrame;//当前读取到那一帧,总帧数相关
    
//开启线程
mReadThread = new ReadThread();
mReadThread.start();
mScheduler = new Scheduler(duration, mFrameCount, new FrameUpdateListener());

private class ReadThread extends Thread {
    @Override
    public void run() {
        try {
            while (mReadFrame < mFrameCount) {//并且没有读完则继续读取
                if (mCacheBitmaps.size() >= MAX_CACHE_NUMBER) {//如果读取的超过最大缓存则暂停读取
                    SystemClock.sleep(1);
                    continue;
                }

                Bitmap bmp = readBitmap(mPaths[mReadFrame]);
                mCacheBitmaps.add(bmp);

                mReadFrame++;

                if (mReadFrame == 1) {//读取到第一帧后在开始调度器
                    mScheduler.start();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

private class FrameUpdateListener implements OnFrameUpdateListener {
    @Override
    public void onFrameUpdate(long frameIndex) {
        if (mCacheBitmaps.isEmpty()) {//如果当前没有帧,则直接跳过
            return;
        }

        Bitmap bitmap = mCacheBitmaps.remove(0);//获取第一帧同时从缓存里删除
        drawBitmap(bitmap);
        recycleBitmap(bitmap);
    }
}

事实上现在还有一个比较严重的问题,这个问题就是内存抖动。内存抖动是指在短时间内有大量的对象被创建或者被回收的现象。意思就是你在循环中或者onDraw()被频繁运行的方法中去创建对象,结果导致频繁的gc,而gc会导致线程卡顿,如果你在onDraw()或者onLayout()方法中去创建对象,AS应该会提示你(Avoid object allocations during draw/layout operations (preallocate and reuse instead))。要解决这个问题必须尽可能的减少创建对象,去复用之前已经创建的对象,这一点我们可以通过创建对象池解决,可是我们要如何才能复用Bitmap?

在BitmapFactory.Options对象中有个inBitmap属性,如果你设置inBitmap等于某个Bitmap(当然这里有限制,上面的文章已经讲的很清楚了),你在用这个BitmapFactory.Options去加载Bitmap,它就会复用这块内存,如果这个Bitmap在绘制中,你有可能会看见撕裂现象。 我们要做的就是创建一个Bitmap对象池,将已经画完的Bitmap放回对象池,当我们要读取的时候,从对象池中获取合适的对象赋予inBitmap。

先看下BitmapFactory.Options里我们使用的主要属性

inBitmap:如果该值不等于空,则在解码时重新使用这个Bitmap。

inMutable:Bitmap是否可变的,如果设置了inBitmap,该值必须为trueinPreferredConfig:指定解码颜色格式。

inJustDecodeBounds:如果设置为true,将不会将图片加载到内存中,但是可以获得宽高。

inSampleSize:图片缩放的倍数,如果设置为2代表加载到内存中的图片大小为原来的2分之一,这个值总是和inJustDecodeBounds配合来加载大图片,在这里我直接设置为1,这样做实际上是有问题的,如果图片过大很容易发生OOM。

readBitmap方法修改如下

private Bitmap readBitmap(String path) throws IOException {
    InputStream is = getResources().getAssets().open(path);//这里需要以流的形式读取
    BitmapFactory.Options options = getReusableOptions(is);//获取参数设置
    Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
    is.close();
    return bmp;
}

//实现复用,
private BitmapFactory.Options getReusableOptions(InputStream is) throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    options.inSampleSize = 1;
    options.inJustDecodeBounds = true;//这里设置为不将图片读取到内存中
    is.mark(is.available());
    BitmapFactory.decodeStream(is, null, options);//获得大小
    options.inJustDecodeBounds = false;//设置回来
    is.reset();
    Bitmap inBitmap = getBitmapFromReusableSet(options);
    options.inMutable = true;
    if (inBitmap != null) {//如果有符合条件的设置属性
        options.inBitmap = inBitmap;
    }
    return options;
}

//从复用池中寻找合适的bitmap
private Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
    if (mReusableBitmaps.isEmpty()) {
        return null;
    }
    int count = mReusableBitmaps.size();
    for (int i = 0; i < count; i++) {
        Bitmap item = mReusableBitmaps.get(i);
        if (ImageUtil.canUseForInBitmap(item, options)) {//寻找符合条件的bitmap
            return mReusableBitmaps.remove(i);
        }
    }
    return null;
}

/*
 *  判断该Bitmap是否可以设置到BitmapFactory.Options.inBitmap上
 */
public static boolean canUseForInBitmap(Bitmap bitmap, BitmapFactory.Options options) {
    // 在Android4.4以后,如果要使用inBitmap的话,只需要解码的Bitmap比inBitmap设置的小就行了,对inSampleSize没有限制
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        int width = options.outWidth;
        int height = options.outHeight;
        if (options.inSampleSize > 0) {
            width /= options.inSampleSize;
            height /= options.inSampleSize;
        }
        int byteCount = width * height * getBytesPerPixel(bitmap.getConfig());
        return byteCount <= bitmap.getAllocationByteCount();
    }
    // 在Android4.4之前,如果想使用inBitmap的话,解码的Bitmap必须和inBitmap设置的宽高相等,且inSampleSize为1
    return bitmap.getWidth() == options.outWidth
            && bitmap.getHeight() == options.outHeight
            && options.inSampleSize == 1;
}

SurfaceView 还是 TextureView?

因为 TextureView 是 View 层次结构的固有成员,所以其行为与其他所有 View 一样,可以与其他元素相互叠加。您可以执行任意转换,并通过简单的 API 调用将内容检索为位图。

影响 TextureView 的主要因素是合成步骤的表现。使用 SurfaceView 时,内容可以写到 SurfaceFlinger(理想情况下使用叠加层)合成的独立分层中。使用 TextureView 时,View 合成往往使用 GLES 执行,并且对其内容进行的更新也可能会导致其他 View 元素重绘(例如,如果它们位于 TextureView 上方)。View 呈现完成后,应用界面层必须由 SurfaceFlinger 与其他分层合成,以便您可以高效地将每个可见像素合成两次。对于全屏视频播放器,或任何其他相当于位于视频上方的界面元素的应用,SurfaceView 可以带来更好的效果。

如之前所述,受 DRM 保护的视频只能在叠加平面上呈现。支持受保护内容的视频播放器必须使用 SurfaceView 进行实现。

案例研究:Grafika 的视频播放 (TextureView)

Grafika 包括一对视频播放器,一个用 TextureView 实现,另一个用 SurfaceView 实现。对于这两个视频播放器来说,仅将帧从 MediaCodec 发送到 Surface 的视频解码部分是一样的。这两种实现之间最有趣的区别是呈现正确宽高比所需的步骤。

SurfaceView 需要 FrameLayout 的自定义实现,而要重新调整 SurfaceTexture 的大小,只需使用 TextureView#setTransform() 配置转换矩阵即可。对于前者,您会通过 WindowManager 向 SurfaceFlinger 发送新的窗口位置和大小值;对于后者,您仅仅是在以不同的方式呈现它。

否则,两种实现均遵循相同的模式。创建 Surface 后,系统会启用播放。点击“播放”时,系统会启动视频解码线程,并将 Surface 作为输出目标。之后,应用代码不需要执行任何操作,SurfaceFlinger(适用于 SurfaceView)或 TextureView 会处理合成和显示。

案例研究:Grafika 的双重解码

此操作组件演示了在 TextureView 中对 SurfaceTexture 的操控。

此操作组件的基本结构是一对显示两个并排播放的不同视频的 TextureView。为了模拟视频会议应用的需求,我们希望在操作组件因屏幕方向发生变化而暂停和恢复时,MediaCodec 解码器能保持活动状态。原因在于,如果不对 MediaCodec 解码器使用的 Surface 进行完全重新配置,就无法更改它,而这是成本相当高的操作;因此我们希望 Surface 保持活动状态。Surface 只是 SurfaceTexture 的 BufferQueue 中生产者界面的句柄,而 SurfaceTexture 由 TextureView 管理;因此我们还需要 SurfaceTexture 保持活动状态。那么我们如何处理 TextureView 被关闭的情况呢?

TextureView 提供的 setSurfaceTexture() 调用正好能够满足我们的需求。我们从 TextureView 获取对 SurfaceTexture 的引用,并将它们保存在静态字段中。当操作组件被关闭时,我们从 onSurfaceTextureDestroyed() 回调返回“false”,以防止 SurfaceTexture 被销毁。当操作组件重新启动时,我们将原来的 SurfaceTexture 填充到新的 TextureView 中。TextureView 类负责创建和破坏 EGL 上下文。

每个视频解码器都是从单独的线程驱动的。乍一看,我们似乎需要每个线程的本地 EGL 上下文;但请注意,具有解码输出的缓冲区实际上是从 mediaserver 发送给我们的 BufferQueue 消费者 (SurfaceTexture)。TextureView 会为我们处理呈现,并在界面线程上执行。

使用 SurfaceView 实现该操作组件可能较为困难。我们不能只创建一对 SurfaceView 并将输出引导至它们,因为 Surface 在屏幕方向改变期间会被销毁。此外,这样做会增加两个层,而由于可用叠加层的数量限制,我们不得不尽量将层数量减到最少。与上述方法不同,我们希望创建一对 SurfaceTexture,以从视频解码器接收输出,然后在应用中执行呈现,使用 GLES 将两个纹理间隙呈现到 SurfaceView 的 Surface。

SurfaceTexture 工作方式之 VideoDumpView.java

这个例子的效果是从MediaPlayer中拿到视频帧,然后显示在屏幕上,接着把屏幕上的内容dump到指定文件中。因为SurfaceTexture本身只产生纹理,所以这里还需要GLSurfaceView配合来做最后的渲染输出。

首先,VideoDumpView是GLSurfaceView的继承类。在构造函数VideoDumpView()中会创建VideoDumpRenderer,也就是GLSurfaceView.Renderer的实例,然后调setRenderer()将之设成GLSurfaceView的Renderer。

109    public VideoDumpView(Context context) {  
...  
116        mRenderer = new VideoDumpRenderer(context);  
117        setRenderer(mRenderer);  
118    }  

随后,GLSurfaceView中的GLThread启动,创建EGL环境后回调VideoDumpRenderer中的onSurfaceCreated()。

519        public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {  
...  
551            // Create our texture. This has to be done each time the surface is created.  
552            int[] textures = new int[1];  
553            GLES20.glGenTextures(1, textures, 0);  
554  
555            mTextureID = textures[0];  
556            GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTextureID);  
...  
575            mSurface = new SurfaceTexture(mTextureID);  
576            mSurface.setOnFrameAvailableListener(this);  
577  
578            Surface surface = new Surface(mSurface);  
579            mMediaPlayer.setSurface(surface);  

这里,首先通过GLES创建GL的外部纹理。外部纹理说明它的真正内容是放在ion分配出来的系统物理内存中,而不是GPU中,GPU中只是维护了其元数据。接着根据前面创建的GL纹理对象创建SurfaceTexture。

SurfaceTexture的参数为GLES接口函数glGenTexture()得到的纹理对象id。在初始化函数SurfaceTexture_init()中,先创建GLConsumer和相应的BufferQueue,再将它们的指针通过JNI放到SurfaceTexture的Java层对象成员中。

230 static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached,  
231        jint texName, jboolean singleBufferMode, jobject weakThiz)  
232 {  
...  
235    BufferQueue::createBufferQueue(&producer, &consumer);  
...  
242    sp<GLConsumer> surfaceTexture;  
243    if (isDetached) {  
244        surfaceTexture = new GLConsumer(consumer, GL_TEXTURE_EXTERNAL_OES,  
245                true, true);  
246    } else {  
247        surfaceTexture = new GLConsumer(consumer, texName,  
248                GL_TEXTURE_EXTERNAL_OES, true, true);  
249    }  
...  
256    SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture);  
257    SurfaceTexture_setProducer(env, thiz, producer);  
...  
266    sp<JNISurfaceTextureContext> ctx(new JNISurfaceTextureContext(env, weakThiz,  
267            clazz));  
268    surfaceTexture->setFrameAvailableListener(ctx);  
269    SurfaceTexture_setFrameAvailableListener(env, thiz, ctx);  

由于直接的Listener在Java层,而触发者在Native层,因此需要从Native层回调到Java层。这里通过JNISurfaceTextureContext当了跳板。JNISurfaceTextureContext的onFrameAvailable()起到了Native和Java的桥接作用:

180void JNISurfaceTextureContext::onFrameAvailable()  
...  
184        env->CallStaticVoidMethod(mClazz, fields.postEvent, mWeakThiz); 

其中的fields.postEvent早在SurfaceTexture_classInit()中被初始化为SurfaceTexture的postEventFromNative()函数。这个函数往所在线程的消息队列中放入消息,异步调用VideoDumpRenderer的onFrameAvailable()函数,通知VideoDumpRenderer有新的数据到来。

回到onSurfaceCreated(),接下来创建供外部生产者使用的Surface类。Surface的构造函数之一带有参数SurfaceTexture。

133    public Surface(SurfaceTexture surfaceTexture) {  
...  
140            setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture));  

它实际上是把SurfaceTexture中创建的BufferQueue的Producer接口实现类拿出来后创建了相应的Surface类。

135 static jlong nativeCreateFromSurfaceTexture(JNIEnv* env, jclass clazz,  
136        jobject surfaceTextureObj) {  
137    sp<IGraphicBufferProducer> producer(SurfaceTexture_getProducer(env, surfaceTextureObj));  
...  
144    sp<Surface> surface(new Surface(producer, true));  

这样,Surface为BufferQueue的Producer端,SurfaceTexture中的GLConsumer为BufferQueue的Consumer端。当通过Surface绘制时,SurfaceTexture可以通过updateTexImage()来将绘制结果绑定到GL的纹理中。

回到onSurfaceCreated()函数,接下来调用setOnFrameAvailableListener()函数将VideoDumpRenderer(实现SurfaceTexture.OnFrameAvailableListener接口)作为SurfaceTexture的Listener,因为它要监听内容流上是否有新数据。接着将SurfaceTexture传给MediaPlayer,因为这里MediaPlayer是生产者,SurfaceTexture是消费者。后者要接收前者输出的Video frame。这样,就通过Observer pattern建立起了一条通知链:MediaPlayer -> SurfaceTexture -> VideDumpRenderer。在onFrameAvailable()回调函数中,将updateSurface标志设为true,表示有新的图像到来,需要更新Surface了。为毛不在这儿马上更新纹理呢,因为当前可能不在渲染线程。SurfaceTexture对象可以在任意线程被创建(回调也会在该线程被调用),但updateTexImage()只能在含有纹理对象的GL context所在线程中被调用。因此一般情况下回调中不能直接调用updateTexImage()。

与此同时,GLSurfaceView中的GLThread也在运行,它会调用到VideoDumpRenderer的绘制函数onDrawFrame()。

372        public void onDrawFrame(GL10 glUnused) {  
...  
377                if (updateSurface) {  
...  
380                    mSurface.updateTexImage();  
381                    mSurface.getTransformMatrix(mSTMatrix);  
382                    updateSurface = false;  
...  
394            // Activate the texture.  
395            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);  
396            GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTextureID);  
...  
421            // Draw a rectangle and render the video frame as a texture on it.  
422            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);  
...  
429                DumpToFile(frameNumber);  

这里,通过SurfaceTexture的updateTexImage()将内容流中的新图像转成GL中的纹理,再进行坐标转换。绑定刚生成的纹理,画到屏幕上。

最后onDrawFrame()调用DumpToFile()将屏幕上的内容倒到文件中。在DumpToFile()中,先用glReadPixels()从屏幕中把像素数据存到Buffer中,然后用FileOutputStream输出到文件。

TextureView 工作方式之 LiveCameraActivity.java

它它可以将Camera中的内容放在View中进行显示。在onCreate()函数中首先创建TextureView,再将Activity(实现了TextureView.SurfaceTextureListener接口)传给TextureView,用于监听SurfaceTexture准备好的信号。

protected void onCreate(Bundle savedInstanceState) {  
    ...  
    mTextureView = new TextureView(this);  
    mTextureView.setSurfaceTextureListener(this);  
    ...  
}  

TextureView的构造函数并不做主要的初始化工作。主要的初始化工作是在getHardwareLayer()中,而这个函数是在其基类View的draw()中调用。TextureView重载了这个函数:

348    HardwareLayer getHardwareLayer() {  
...  
358            mLayer = mAttachInfo.mHardwareRenderer.createTextureLayer();  
359            if (!mUpdateSurface) {  
360                // Create a new SurfaceTexture for the layer.  
361                mSurface = new SurfaceTexture(false);  
362                mLayer.setSurfaceTexture(mSurface);  
363            }  
364            mSurface.setDefaultBufferSize(getWidth(), getHeight());  
365            nCreateNativeWindow(mSurface);  
366  
367            mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);  
368  
369            if (mListener != null && !mUpdateSurface) {  
370                mListener.onSurfaceTextureAvailable(mSurface, getWidth(), getHeight());  
371            }  
...  
390        applyUpdate();  
391        applyTransformMatrix();  
392  
393        return mLayer;  
394    }  

因为TextureView是硬件加速层(类型为LAYER_TYPE_HARDWARE),它首先通过HardwareRenderer创建相应的HardwareLayer类,放在mLayer成员中。然后创建SurfaceTexture类。之后将HardwareLayer与SurfaceTexture做绑定。接着调用Native函数nCreateNativeWindow,它通过SurfaceTexture中的BufferQueueProducer创建Surface类。注意Surface实现了ANativeWindow接口,这意味着它可以作为EGL Surface传给EGL接口从而进行硬件绘制。然后setOnFrameAvailableListener()将监听者mUpdateListener注册到SurfaceTexture。这样,当内容流上有新的图像到来,mUpdateListener的onFrameAvailable()就会被调用。然后需要调用注册在TextureView中的SurfaceTextureListener的onSurfaceTextureAvailable()回调函数,通知TextureView的使用者SurfaceTexture已就绪。

注意这里这里为TextureView创建了DeferredLayerUpdater,而不是像Android 4.4(Kitkat)中返回GLES20TextureLayer。因为Android 5.0(Lollipop)中在App端分离出了渲染线程,并将渲染工作放到该线程中。这个线程还能接收VSync信号,因此它还能自己处理动画。事实上,这里DeferredLayerUpdater的创建就是通过同步方式在渲染线程中做的。DeferredLayerUpdater,顾名思义,就是将Layer的更新请求先记录在这,当渲染线程真正要画的时候,再进行真正的操作。其中的setSurfaceTexture()会调用HardwareLayer的Native函数nSetSurfaceTexture()将SurfaceTexture中的surfaceTexture成员(类型为GLConsumer)传给DeferredLayerUpdater,这样之后要更新纹理时DeferredLayerUpdater就知道从哪里更新了。

前面提到初始化中会调用onSurfaceTextureAvailable()这个回调函数。在它的实现中,TextureView的使用者就可以将准备好的SurfaceTexture传给数据源模块,供数据源输出之用。如:

public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {  
    mCamera = Camera.open();  
        ...  
        mCamera.setPreviewTexture(surface);  
        mCamera.startPreview();  
        ...  
}  

看一下setPreviewTexture()的实现,其中把SurfaceTexture中初始化时创建的GraphicBufferProducer拿出来传给Camera模块。

576static void android_hardware_Camera_setPreviewTexture(JNIEnv *env,  
577        jobject thiz, jobject jSurfaceTexture)  
...  
585        producer = SurfaceTexture_getProducer(env, jSurfaceTexture);  
...  
594    if (camera->setPreviewTarget(producer) != NO_ERROR) {  

接下来当内容流有新图像可用,TextureView会被通知到(通过SurfaceTexture.OnFrameAvailableListener接口)。SurfaceTexture.OnFrameAvailableListener是SurfaceTexture有新内容来时的回调接口。TextureView中的mUpdateListener实现了该接口:

755        public void onFrameAvailable(SurfaceTexture surfaceTexture) {  
756            updateLayer();  
757            invalidate();  
758        }  

可以看到其中会调用updateLayer()函数,然后通过invalidate()函数申请更新UI。updateLayer()会设置mUpdateLayer标志位。这样,当下次VSync到来时,Choreographer通知App通过重绘View hierachy。在UI重绘函数performTranversals()中,作为View hierachy的一分子,TextureView的draw()函数被调用,其中便会相继调用applyUpdate()和HardwareLayer的updateSurfaceTexture()函数。

138    public void updateSurfaceTexture() {  
139        nUpdateSurfaceTexture(mFinalizer.get());  
140        mRenderer.pushLayerUpdate(this);  
141    }  

updateSurfaceTexture()实际通过JNI调用到android_view_HardwareLayer_updateSurfaceTexture()函数。在其中会设置相应DeferredLayerUpdater的标志位mUpdateTexImage,它表示在渲染线程中需要更新该层的纹理。

ThreadedRenderer作为新的HardwareRenderer替代了Android 4.4中的Gl20Renderer。其中比较关键的是RenderProxy类,需要让渲染线程干活时就通过这个类往渲染线程发任务。RenderProxy中指向的RenderThread就是渲染线程的主体了,其中的threadLoop()函数是主循环,大多数时间它会poll在线程的Looper上等待,当有同步请求(或者VSync信号)过来,它会被唤醒,然后处理TaskQueue中的任务。TaskQueue是RenderTask的队列,RenderTask代表一个渲染线程中的任务。如DrawFrameTask就是RenderTask的继承类之一,它主要用于渲染当前帧。而DrawFrameTask中的DeferredLayerUpdater集合就存放着之前对硬件加速层的更新操作申请。

当主线程准备好渲染数据后,会以同步方式让渲染线程完成渲染工作。其中会先调用processLayerUpdate()更新所有硬件加速层中的属性,继而调用到DeferredLayerUpdater的apply()函数,其中检测到标志位mUpdateTexImage被置位,于是会调用doUpdateTexImage()真正更新GL纹理和转换坐标。

总结

SurfaceView是一个有自己Surface的View。它的渲染可以放在单独线程而不是主线程中。其缺点是不能做变形和动画(这里说的变形和动画应该指的是View整体的变形和动画, 因为SurfaceView内是完全可以实现动态的图像变化的)。SurfaceTexture可以用作非直接输出的内容流,这样就提供二次处理的机会。与SurfaceView直接输出(直接显示出来)相比,这样会有若干帧的延迟。同时,由于它本身管理BufferQueue,因此内存消耗也会稍微大一些。TextureView是一个可以把内容流作为外部纹理输出在上面的View。它本身需要是一个硬件加速层。事实上TextureView本身也包含了SurfaceTexture。它与SurfaceView+SurfaceTexture组合相比可以完成类似的功能(即把内容流上的图像转成纹理,然后输出)。区别在于TextureView是在View hierachy中做绘制,因此一般它是在主线程上做的(在Android 5.0引入渲染线程后,它是在渲染线程中做的)。而SurfaceView+SurfaceTexture在单独的Surface上做绘制,可以是用户提供的线程,而不是系统的主线程或是渲染线程。另外,与TextureView相比,它还有个好处是可以用Hardware overlay进行显示。

什么是hardware overlay? 基本上就是说SurfaceView+SurfaceTexture可以组合在一起, TextureView和SurfaceTexture也可以组合在一起, 这两种组合一个是在自定义线程中, 一个是个主线程(5.0之后也不是主线程而是渲染 线程)中。

游戏循环

实现游戏循环的热门方法如下所示:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

此方法还存在一些问题,最根本的问题是游戏可以定义什么是“帧”这一情况。不同的显示屏将以不同的速率刷新,并且该速率可能会随时间变化。如果您生成帧的速度快于显示屏能够显示的速度,则您偶尔不得不丢掉一个帧。如果生成帧的速度过慢,则 SurfaceFlinger 会定期无法找到新的缓冲区来获取帧,并将重新显示上一帧。这两种情况都会导致明显异常。

您要做的是匹配显示屏的帧速率,并根据从上一帧起经过的时间推进游戏状态。有两种方法可以实现这一点:(1) 将 BufferQueue 填满并依赖“交换缓冲区”背压;(2) 使用 Choreographer (API 16+)。

队列填充

只需尽快交换缓冲区,即可轻松实现队列填充。在 Android 的早期版本中,这样做实际上可能会使您遭受处罚,其中 SurfaceView#lockCanvas() 会让您休眠 100 毫秒。现在,它由 BufferQueue 调节,且 BufferQueue 的清空速度与 SurfaceFlinger 的消费能力相关。

可在 Android Breakout 中找到此方法的一个示例。它使用 GLSurfaceView,后者在一个循环中运行,而该循环会调用应用的 onDrawFrame() 回调,然后交换缓冲区。如果 BufferQueue 已满,则直到缓冲区可用之后才会调用 eglSwapBuffers()。当 SurfaceFlinger 获取一个新的用于显示的缓冲区后,便会释放之前获取的缓冲区,这时这些缓冲区就变为可用状态。因为这发生在 VSYNC 上,所以您的绘图循环时间将与刷新率相匹配。大多数情况下是这样的。

此方法存在几个问题。首先,应用与 SurfaceFlinger 操作组件关联,后者所花费的时间各不相同,具体取决于要执行的工作量以及是否与其他进程抢占 CPU 时间。由于您的游戏状态根据缓冲区交换的间隔时间推进,因此动画不会以一致的速率更新。但是以 60fps 的速率运行时,不一致会在一段时间之后达到平衡,因此您可能不会注意到卡顿。

其次,由于 BufferQueue 尚未填满,因此前几次缓冲区交换的速度会非常快。所计算的帧间隔时间将接近于零,因此游戏会生成几个不会发生任何操作的帧。在 Breakout 这样的游戏(每次刷新都会更新屏幕)中,除了游戏首次启动(或取消暂停)时之外,队列总是满的,因此效果不明显。偶尔暂停动画,然后返回到快速模式的游戏可能会出现异常问题。

Choreographer

通过 Choreographer,您可以设置在下一个 VSYNC 上触发的回调。实际的 VSYNC 时间以参数形式传入。因此,即使您的应用不会立即唤醒,您仍然可以准确了解显示屏刷新周期何时开始。使用此值(而非当前时间)可为您的游戏状态更新逻辑产生一致的时间源。

遗憾的是,您在每个 VSYNC 之后得到回调这一事实并不能保证及时执行回调,也无法保证您能够迅速高效地对其进行操作。您的应用需要手动检测卡顿和丢帧的情况。

Grafika 中的“记录 GL 应用”操作组件提供了此情况的示例。在某些设备(例如 Nexus 4 和 Nexus 5)上,如果您只是坐视不理,则操作组件会开始丢帧。GL 呈现微不足道,但有时会重新绘制 View 元素;如果设备已进入降低功耗模式,则测量/布局传递可能需要很长时间(根据 systrace,Android 4.4 上的时钟速度减慢之后,这一传递需要 28 毫秒,而不是 6 毫秒。如果您在屏幕上拖动手指,它会认为您在与该操作组件互动,因此时钟会保持高速状态,您永远不会丢帧)。

如果当前时间超过 VSYNC 时间后 N 毫秒,则简单的解决办法是在 Choreographer 回调中丢掉一帧。理想情况下,N 的值取决于先前观察到的 VSYNC 间隔。例如,如果刷新周期是 16.7 毫秒 (60fps),而您的运行时间延迟超过 15 毫秒,则可能会丢失一帧。

如果您查看“记录 GL 应用”运行情况,则会看到丢帧计数器计数增加了,甚至会在丢帧时在边框中看到红色闪烁情况。除非您的观察力非常强,否则不会看到动画卡顿现象。以 60fps 的速率运行时,只要动画以固定速率继续播放,应用可以偶尔丢帧,没有任何人会注意到。您成功的几率在一定程度上取决于您正在绘制的内容、显示屏的特性,以及使用该应用的用户发现卡顿的擅长程度。

线程管理

一般而言,如果您要呈现到 SurfaceView、GLSurfaceView 或 TextureView 上,则需要在专用线程中进行呈现。请勿在界面线程上进行任何“繁重”或花费时间不定的操作。

Breakout 和“记录 GL 应用”使用专用呈现程序线程,并且也在该线程上更新动画状态。只要可以快速更新游戏状态,这就是合理的方法。

其他游戏将游戏逻辑和呈现完全分开。如果您有一个简单的游戏,只是每 100 毫秒移动一个块,则您可以使用一个只进行以下操作的专用线程:

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(您可能需要让休眠时间基于固定的时钟,以防止漂移现象 - sleep() 不完全一致,并且 moveBlock() 需要花费的时间不为零 - 但是您了解就行。)

当绘图代码唤醒时,它就会抓住锁,获取块的当前位置,释放锁,然后进行绘制。您无需基于帧间增量时间进行分数移动,只需要有一个移动对象的线程,以及另一个在绘制开始时随地绘制对象的线程。

对于任何复杂度的场景,都需要创建一个即将发生的事件的列表(按唤醒时间排序),并使绘制代码保持休眠状态,直到该发生下一个事件为止。