Android 图形架构


转载地址 : http://blog.csdn.net/new_szsheep/article/details/41348581


图形架构


每一个开发者都应该知道Surface, SurfaceHolder, EGLSurface, SurfaceView, GLSurfaceView, SurfaceTexture, TextureView 以及 SurfaceFlinger。


这篇文章主要描述了Android系统级图形架构的必要元素,以及如何被应用框架以及多媒体系统应用。这里主要集中说明图形buffer如何在系统间

流转。如果你想了解SurfaceView以及TextureView为什么如此运行,以及Surface和EGLSurface如何交互,那你就来对地方了。


这里假设你掌握一些Android设备以及应用开发的知识概念,但不需要详细了解应用框架,同时这里提及的API细节不多。这里的内容与其他

公开的文章没有太多重叠。这文章的目标是提供一种对图形从如何着色到输出中涉及事件的认知,好让你在开发应用时候可以获得一些建议选择。

要达到此目的,我们要置底而上的描述UI类如何工作而不是如何使用。


文章的前面部分涉及到一些后面会用到的背景材料,故此最好就是从头到尾的阅读此文章,不要跳到感兴趣的部分。我会从解释Android图形Buffer开始,

接着描述合成以及显示的机制,在进一步了解高层提供合成数据的机制。


这文章主要涉及Android 4.4的系统,与早期的版本工作机制有区别,甚至未来很有可能也不一样。跟版本相关的特性会在文章一些地方给指出。


从多方面,我都建议参考Grafika的开源代码,这是google为测试而弄的开源项目,它比看例子更快的了解系统。


BufferQueue 和 gralloc

要了解Android的图形系统如何工作,我们要从场景的背后开始。Android所有围绕图形相关的中心点是一个成为BufferQueue的类。它的作用十分的简单:

把提供图形数据buffer的生产者与接受图形数据并显示或进一步处理的消费者连接起来。生产者与消费者可以存在与不同的进程。几乎所有涉及到在图形

系统中移动的事情,都依赖BufferQueue。


基本的用法很直接。生产者请求一个空的buffer(dequeueBuffer()),然后给此buffer定义一些特性,如宽,高,像素格式以及使用标志。生产者然后对此buffer

植入内容,并把它归还到队列(queueBuffer())。过后,消费者从队列获取buffer(acquireBuffer()) 并使用。当消费者用完后,它会把buffer归还给队列(releaseBuffer())。


大部分最近的Android设备都支持"sync Framework"。这允许具备硬件异步处理图形数据的系统很好工作。举个例子,生产者可以提交了一系列OpenGL ES的

绘画命令,然后在着色未完成时把buffer放进队列。buffer带有一个可以提示内容准备好的(fence)围栏。另一种(fence)围栏可以在buffer被归还到空列表时候附上,

好让消费者可以在buffer还在用的时候释放buffer。当buffer在系统中移动时,这种方法提升了延迟以及吞吐量。


队列的一些特性,如它能维持的最大数目buffer,是由生产者和消费者联合决定的。


有需要时候,BufferQueue会创建buffer来响应。buffer会一直保留直到其特性改变。举个例子,如果生产者开始请求不同大小的buffer,则旧buffer会被释放,新的

buffer会被按需创建。


数据结构目前总是由消费者创建以及“拥有”。在Android 4.3里,仅仅生产者是Binder化,也就是说生产者可能存在在另一个进程,而消费者与创建队列的进程同属一个进程。

在4.4版本,这个改进了一点,朝着更大众化的去实现。


buffer内容从不由BufferQueue拷贝。移动那么大量的数据是低效的。因此,buffer总是以句柄的方式移动。


gralloc HAL

实际的buffer创建是通过被称为"gralloc"内存创建器,这个是按照设备商指定的接口来实现的。alloc函数接受如你期望的参数:宽,高,像素格式以及一套使用标志。

这些使用标志值得进一步关注。


gralloc创建器并不只是另外一种在堆上的内存创建器。在某些情况,创建的内容也许不是与buffer一致或完全不能在用户空间访问。创建的本质是由使用标志觉定的,

如下面属性:

  • 内存如何经常从软件上访问(CPU)
  • 内存如何经常从硬件上访问(GPU)
  • 是否内存被作为一个OpenGL ES纹理被使用
  • 是否内存被视频编码器使用
举个例子,如果你指定像素格式为RGBA 8888,以及表明buffer可以从软件访问---意味着你的应用可以直接接触像素---然后,创建器就需要创建一个由4字节代表
一个像素点的buffer。如果相反,你指明此缓存仅仅只能从硬件访问并且作为GLES的纹理,则创建器可以做任何GLES驱动可以做的事---BGRA 次序,非线性
"swizzled" 布局,可替换颜色格式等。允许硬件使用它期望的格式可以提高性能。


某些值不能在特定的平台上被组合。举个例子,”视频编码器“ 标志也许需要 YUV 像素, 所以如果增加” 从软件访问“ 和 指定 RGBA 8888 的格式会导致操作失败。

由gralloc创建器返回的句柄可以在进程间通过Binder的方式传递。


SurfaceFlinger 和 Hardware Composer

拥有图形数据buffer是美好的事,让他在你的设备屏幕显示出来会是更好的事情。这就需要SurfaceFlinger和Hardware Composer HAL。

SurfaceFlinger的作用就是从多头源那里接收buffer数据,合成他们并发往显示器。曾几何时,这个动作是由软件把数据贴到硬件帧buffer来完成的,但这已是很久

以前的事情。


当一个应用调到前景,WindowManager服务就会要求SurfaceFlinger来绘制表面。SurfaceFlinger会创建图层,它的主要构件是BufferQueue,对于这个BufferQueue,

SurfaceFlinger是作为消费者角色。一个生产者的Binder 对象被从WindowManager传到应用,借助此Binder可以让应用开始发送帧到SurfaceFlinger。(注意:

WindowManager 使用术语 ”window“ 而不是 ”Layer“,”Layer" 对其来说意味着其他东西。我们将会暂时的使用SurfaceFlinger术语。对于SurfaceFlinger应该实际被叫做

LayerFlinger这点是存在争议的)


对于大多数应用,任何时候在屏幕上总是存在三个层:位于屏幕顶部的状态栏,位于屏幕底部的导航栏,和应用自身的UI。其他一些应用会比这多或少。

举个例子:默认的Home应用有一个分离的层作为墙纸,而全屏游戏也许会隐藏状态栏。每一个图层都可以独立刷新。状态栏和导航栏由系统进程来着色,

而应用图层则由app着色,两者之间无协调。


设备显示总是在一定频率下刷新的,典型的手机、平板是60帧每秒。如果显示内容以一半频率刷新,”tearing(撕裂)“就很明显了。所以,在两个刷新间隔间更新内容

显得很重要。系统会接收来自显示器的信号,通知其可以安全更新内容。因为历史原因,我们都称这个为 VSYNC 信号。


刷新率也许会随时间变化,举个例子,一些移动设备会在58到62之间变动,依赖于当前的情况。对于HDMI付着的电视,这个理论上下降到24到48帧率来适应视频。

因为我们只能在一个刷新间隔更新屏幕一次,以200的帧率提交buffer给显示器将会是浪费,因为大部分的帧从不会看得到。SurfaceFlinger只有当显示器准备好接收新东西之

后才会唤醒,而不是应用一提交buffer就开始执行。


当VSYNC信号到达,SurfaceFlinger会遍历它的图层列表来找新buffer(提交)。如果找到一个新的,则获取出来。如果没,他继续使用之前获取的buffer。SurfaceFlinger

总是要有东西去显示,故此它总会挂着一个buffer。如果从没有图层上的buffer提交,则此图层会被忽略。


一旦SurfaceFlinger收集到所有可见图层的buffer,它会询问Hardware Composer如何来组合。


Hardware Composer

Hardware Composer HAL ("HWC") 首次在Android 3.0被引进,经过数年已经变得很稳定了。它主要的目的是选择最高效的途径来合成buffer。作为HAL,它的实现是依赖

设备的,并通常由OEM显示硬件厂家完成。


这个方法的价值在于可以很容易的识别你该什么时候用”Overlay planes“。Overlay plane的使用目的就是把多个buffer在显示器而不是GPU那里合成起来。举个例子,假设你

有一台Android手机竖着摆,屏幕上有状态栏和导航栏以及其他地方是应用UI。每一个图层的内容都在分开的buffer里。你可以先把应用的内容画好在一个草稿图层

那里,接着把状态栏的图层着色,接着是导航栏,最后把草稿图层的buffer发往显示设备。又或者,你可以把三个buffer都送到显示设备硬件,然后告知它从三个不同的buffer

获取内容,在屏幕的不同部分进行着色。明显,最后的办法更高效。


正如你所想的,不同显示设备的处理能力明显有区别。overlay的数量,图层是否可旋转或混合,以及对位置和重叠的束缚会比较困难通过API来展现。因此HWC如下工作:

  1. SurfaceFlinger提供给HWC一个完整的图层列表,并询问”你将如何处理它“
  2. HWC通过在每个图层上标明”重叠 overlay"或”GLES 合成“来响应。
  3. SurfaceFlinger来处理所有的GLES合成,把输出buffer发给HWC并让HWC处理剩余的事情
因为这个决定的代码是可以由硬件提供商客制化,故此这是最有可能获取最高表现。

当屏幕没有更新的情况下,overlay plane比GL合成显得更低效率。这尤其在overlay的内容有透明像素,并且重叠图层混合在一起时候。这种情况,HWC可以选择请求GLES
来合成一些或全部图层,同时保留合成的buffer。如果SurfaceFlinger又回来要求合成同样的buffer组时候,HWC可以仅仅只显示之前合成好的草稿buffer。这可以提高设备待
机时候的电池寿命。

安装Android 4.4的设备一般支持四个overlay plane。尝试合成比存在的图层更多的图层会导致系统使用GLES来合成部分图层。因此,应用使用的图层数量可以对电池消耗以
及性能只有有数的影响。

你可以通过adb shell dumpsys Surfaceflinger的命令来确切的显示SurfaceFlinger的工作情况。结果比较冗长,与我们目前关于HWC讨论相关的内容部分是在结果的底部:

    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
 这表明了屏幕上有哪些图层,这些图层是否被Overlay(HWC)处理或OpenGL ES合成(GLES)处理,以及其他一堆你大概不关心的数据(句柄,提示,标志以及其他我们 
  
已经截去的一些信息) ”source crop“和”frame“的价值将会在迟点更紧密的调查。

FB_TARGET layer是GLES合成的输出地方。因为上述所有图层使用Overlays,所以FB_TARGET没有在这个帧上使用。图层的名字暗示着它原有的用途:在设备上以 
/dev/graphics/fb0存在,无Overlays,所有组合都在GLES完成,并且输出会写到帧buffer。最近的设备,一般说来都没有简单的帧buffer,所以FB_TARGET 图层就是草稿
buffer。(注意:这就是为什么旧版本开发的屏幕捕抓应用无法再凑效:因为他们尝试去读帧buffer,但并不存在这的东西)

Overlay Plane还有其他重要的作用:他们是唯一的方式来显示DRM内容。DRM保护的buffer不能由SurfaceFlinger或GLES驱动访问,意味着如果由HWC切换到GLES组合,你的视频将会不可见。


三个Buffer的必要性

为了避免存在显示的撕裂情况,系统需要双buffer:当后buffer准备时候,前buffer就显示。在VSYNC信号这个点,如果后buffer准备好了,你就要迅速的切换他们。这个在你可

以直接在帧buffer上绘画的系统上是可行的,但当加入合成步骤时候,就会出现显示停顿。因为SurfaceFlinger的触发方式,会导致双buffer管道中出现气泡。


假设第N帧正被显示,第N+1帧已经由SurfaceFlinger取出准备在下一个VSYNC信号发给显示器。(假设帧N是由Overlay来组合的,因此我们不能在显示器没用完此Buffer的情况下更改Buffer的内容。)当VSYNC信号到达时,HWC交替buffer。当应用正开始着色第N+2帧到曾经存储帧N的Buffer时,SurfaceFlinger正扫描图层列表,寻求更新的

Buffer。SurfaceFlinger此时是不会找到任何新Buffer,故此打算准备在下一个VSYNC信号来时又显示N+1帧。过后,应用完成了N+2的着色,并准备入列给SurfaceFlinger,但

已经太迟了。这效果相当于砍了最大帧率的一半。


我们可以通过三个Buffer来弥补。在VSYNC信号前,帧N正在显示,帧N+1已经合成好(或安排给一个Overlay Plane)并且准备显示,帧N+2已经列队准备被SurfaceFlinger获

取。当屏幕翻转,Buffer通过无气泡阶段旋转。应用只有少于一个VSYNC的时间来做着色和入列。SurfaceFlinger/HWC在离下一个翻转时刻之前进行合成时拥有一个完整的

VSYNC时间。下面图显示了对于任何应用想在屏幕上显示东西,至少要花去两个VSYNC周期。随着时延的增加,设备会认为对触摸输入的响应减少了。


图1. SurfaceFlinger + BufferQueue


上图描述了SurfaceFlinger和BufferQueue的流程。

  1. 红Buffer填满后,滑向BufferQueue
  2. 在红Buffer离开应用后,蓝Buffer滑进来,替换红Buffer
  3. 绿Buffer和系统UI滑进HWC(显示SurfaceFlinger依然拥有这些Buffer,但现在HWC已经准备把他们通过叠加方式在下一个VSYNC信号时送到屏幕)
蓝色Buffer即被显示器引用,也被BufferQueue引用。应用app在同步围栏响应前不允许对他进行着色。

在VSYNC信号刻,所以下面的动作同时发生:
  • 红色Buffer跃进SurfaceFlinger,替代绿Buffer
  • 绿Buffer跳进显示,替代蓝Buffer,虚线绿色对出现在BufferQueue(替代之前的蓝Buffer)
  • 蓝色Buffer的围栏发出信号,应用app中的蓝Buffer清空
  • 显示矩形那里从(蓝Buffer+系统UI)变成(绿Buffer+系统UI)
*系统UI的处理提供状态栏和导航栏,在这里没变化。因此SurfaceFlinger保持之前获取的Buffer。实际上,会有两个Buffer,一个给状态栏,一个给导航栏,
他们的大小刚好符合内容。每一个都会入列到自有的BufferQueue里。

**Buffer实际上并不”空“。如果你没对它进行任何绘画处理而提交它,则你会获得一样的蓝色内容。清除Buffer的结果是”空“,这是应用app在开始画之前需要执行的动作。

我们可以减少延迟,通过意识到图层组合不会需要全部的VSYNC信号周期。如果合成是在Overlays那里执行,基本耗费零CPU和GPU时间。但我们不能靠这个,因此我们需要
一点时间。如果应用app在VSYNC信号间隔的半途中开始着色,同时SurfaceFlinger把HWC设置推迟到下一个VSYNC信号即将到达前的数微秒时,我们可以把延迟从2帧降到
1.5帧。理论上你可以着色和组合在一个周期完成,允许返回到Buffer队列去;但让其降那么低,目前设备来说很难做到。在着色,组合以及切换overlays到GLES组合的时间只
要出现轻微的波动,就可以引起我们错过Buffer切换的时间限制,只好重复前一帧。

SurfaceFlinger的Buffer处理示范了之前提及的基于围栏的Buffer管理。如果我们需要全速播放动画,我们需要一个获取好的Buffer作为显示(”前“),和一个获取好的Buffer作为
下一个交替的Buffer(”后“)。如果我们在overlay显示Buffer,则内容会直接由显示访问并不允许被触动。但如果你看在dumpsys SurfaceFlinger的输出的处于激活状态图层
的BufferQueue,你可以看到一个获取出的Buffer,一个排队的Buffer,和一个空Buffer。这是因为当SurfaceFlinger获取一个新”后“Buffer时,它要释放当前的”前“Buffer到队
列。”前“Buffer还在被显示使用中,因此任何取出Buffer的东西都要等待围栏信号达到后才能在其上绘画。只要每一个人都遵循围栏规则,所有的管理队列的IPC消息可以与显
示并发发生。

虚拟显示

SurfaceFlinger支持一个”主“显示,就是内建于你手机或平板上的,同时支持一个“外部”显示,如通过HDMI连接的电视。它也支持一定数量的“虚拟”显示,这些显示使合成的
输出存在系统中。虚拟显示可以被用来录制屏幕或发送到网络。

虚拟显示可以与主显示共享同一套图层,也可以有自己的一套图层。虚拟显示没有VSYNC,因此主显示的VSYNC可被用来作为所有显示合成动作的触发信号。

过去,虚拟显示总是由GLES组合。Hardware Composer只给主显示管理合成。在Android 4.4系统,Hardware Composer拥有参与虚拟显示合成的权利。

如你所想的,由虚拟显示产生的帧都写到一个BufferQueue里。

案例学习:screenrecord

现在,我们已经拥有了BufferQueue和SurfaceFlinger的一些背景知识,对于检视实际的案例很有帮助。

screenrecord命令,在Android 4.4引入,允许你去记录出现在屏幕的任何东西并作为.mp4格式文件存于磁盘。要实现这个功能,我们要接收来自SurfaceFlinger的合成帧,并
把它们送到视频编码器,然后把编码后的数据写入到文件。视频编码器由一个独立的进程管理,称为“mediaserver”,因此我们需要在系统中移动大量的图形buffer。为了增加
挑战,我们尝试以全分辨率记录60fps频率的视频。这里高效工作的关键是BufferQueue。

MediaCodec 类允许应用app提供存在于buffer的原始数据,或通过surface的方式提供。我们迟点才仔细讨论Surface,现在只要把它假设为在BufferQueue一端的生产者封装。
当screenrecord请求访问视频编码器时,mediaserver创建一个BufferQueue并把自己作为它一端的消费者,然后把BufferQueue的生产者一端作为Surface发回给
screenreocrd。

screenrecord命令然后要求SurfaceFlinger创建一个虚拟显示,镜像于主显示(就是拥有主显示的所有图层),并引导其把输出发送到来自Mediaserver的Surface。
注意,这里SurfaceFlinger是Buffer的生产者而不是消费者。

一旦配置完成,screenrecord就可以坐着等待编码数据的出现。当应用app绘制时候,他们的buffer发送到SurfaceFlinger,由它合成到一个buffer发送到mediaserver上的视频
编码器。所有帧对于screenrecord来说是透明的。mediaserver有其自己的方法来移动buffer,通过句柄的方式来减少负载。

案例学习:模拟第二个显示

WindowManager可以请求SurfaceFlinger创建一个可视图层,使SurfaceFlinger作为BufferQueue的消费者。同样也可以要求SurfaceFlinger创建一个虚拟显示,
使SurfaceFlinger作为生产者。如果把这两者联合起码,会如何呢?配置一个虚拟显示来着色虚拟图层?

你创建了一个闭环,在这里,合成的屏幕出现在一个窗口里。当然,这个窗口现在是合成输出的一部分,因此在下一个刷新,在窗口的合成图像会显示窗口的内容。这就是
“驮着一只驮着乌龟的乌龟群”。你可以在设置中激活"Developer option",选择模拟第二显示,并激活一个窗口。作为加分点,我们用screenrecord来捕抓使能显示,来一帧
一帧的播放。

Surface 和 SurfaceHolder

Surface类从1.0开始就作为公共API。它的描述简单表述为 ” 指向一个即将被屏幕合成的原生buffer的句柄“。这个阐述初始写下时比较准确,远远落后于现代系统的标识。
Surface常作为Buffer队列的生产者(但不是总是),由SurfaceFlinger消费。当你在Surface着色时,结果置于一个buffer并被传递到消费者那里。Surface不是简单的一个
你可以在上面涂画的内存块。

显示Surface的BufferQueue典型的被配置为三Buffer;但Buffer是要求时才创建。所以一旦生产者产生Buffer慢--也许在60fps的显示系统中产生30fps的动画---在队列中也许就只有2个Buffer。这可以有助于减少内存消耗。你可以在dumpsys SurfaceFlinger的结果里看到与每个图层相关的Buffer概要。

Canvas Rendering (画布着色)
曾经,所有的着色都是软件执行,现在你依然也可以这样做。底层的实现是由Skia图形库提供。如果你想画一个长方形,你可以调用库函数,正确地在buffer上设置字节。
要保证一个buffer不会由两个客户端来同时更新,或者在显示时刻去被写进内容,你必须在访问时候对其上锁。lockCanvas 对Buffer上锁并返回一个Canvas来用作画画。
unlockcanvasAndPost解锁Buffer并把其发送给合成器。

随着时间推移,带有通用能力的3D引擎设备出现了,Android针对OpenGL ES调整自己。然而,对于应用,框架代码,保持旧API能工作是很重要的。因此有些工作必须在
硬件加速的Canvas的API上努力。就如你在Harware Acceleration页面看到的,这个道路有点崎岖。要特别注意,提供给View的onView方法的Canvas也许是硬件加速,但在
app应用直接用lockCanvas对Surface上锁获得的Canvas从来不是。

当你为了Canvas访问而对Surface上锁时,”CPU 着色器“ 连接到BufferQueue的生产者端,并直到Surface被销毁才解除连接。大部分其他生产者(如GLES)可以解除连接并
再次连接到Surface,但基于画布的”CPU 着色器“则不行。这意味如果你对一个Canvas上锁了,你不能用GLES在Surface上绘画或从视频解码器那里发送帧。

第一次生产者从BufferQueue请求Buffer时,Buffer被创建并初始化为零。初始化是必须的,它可以避免无意的进程间数据共享。然而当你要重新使用Buffer时候,会发现之前的
内容依然出现。如果你重复的调用lockCanvas和unlockCanvasAndPost而没有进行任何绘画,你将会在着色过得帧内循环。

Surface上锁/解锁的代码保持了一份之前着色的Buffer引用。如果当你在上锁Surface时指定一块脏区域,它会拷贝之前Buffer中的非脏的像素。这很大的可能Buffer会由
SurfaceFlinger或HWC处理。但既然我们仅仅读取它,因此没有必要等待来独占访问。

应用直接在Surface上绘画的主要非Canvas方法是通过OpenGL ES。这会在EGLSurface 和 OpenGL ES章节阐述。

SurfaceHolder

与Surface一起工作的东西需要一个SurfaceHolder,尤其是SurfaceView。原本的概念是,Surface代表原始的合成器管理的buffer,而SurfaceHolder由应用app管理,并跟踪

更高一层信息,如维度和格式。Java语言定义与底层本地实现是对应的。可以说,这样分割不再有用,但这成为公共API的一部分很久了。


一般说来,任何与View相关的,都涉及一个SurfaceHolder。一些其他APIs,如MediaCodec,会自己在Surface上操作。你可以很容易的从SurfaceHolder获取Surface,

以便当你拥有它的时候可以传递给后者。


设置和获取Surface参数,如大小,格式的API通过SurfaceHolder来实现。


EGLSurface和OpenGL ES

OpenGL ES定义了着色图形的API。它不是定义窗口系统。为了允许GLES可以在多平台上使用,它被设计成与一个知道如何通过操作系统创建和访问窗口的库结合。Android

用的库叫做EGL。如果你要画多边形图,你调用GLES;如果你要把你着色的图放到屏幕,你调用EGL。


在你使用GLES做任何事前,必须创建一个GL上下文。在EGL中,这意味着创建一个EGLContext和EGLSurface。GLES操作作用与当前上下文,此上下文是由线程本地存储访

问而不是作为一个参数来传递。这意味着你必须注意你当前着色代码执行所在的线程,以及在那个线程上是那个上下文。


EGLSurface可以是一个由EGL创建的离屏Buffer(称为”pbuffer“)或一个由操作系统创建的窗口。EGL窗口surface通过eglCreateWindowSurface函数创建的。它以”window 

object“ 为参数,在android上可以是SurfaceView,SurfaceTexture,SurfaceHolder或Surface,所有这些内里都有一个BufferQueue。当你调用此函数,EGL创建一个新的EGLSurface对象,把其连接到窗口对象的BufferQueue的生产者接口。从那个时候开始,着色那个EGLSurface会使一个Buffer被出列,着色,然后被消费者入列使用。

(术语”window“ 表明被期待的使用,但记住的是,输出不一定是最终出现在显示器)


EGL 并不提供上锁/解锁的调用。相反,你发起绘画命令,然后调用eglSwapBuffers函数来提交当前帧。这个方法名字来自前后Buffer交换的传统,但实际上已经完全不一样

了。

同一时刻仅有一个EGLSurface可以被关联到一个Surface---你只能有一个生产者连接到BufferQueue-----但如果你销毁了EGLSurface,它会解除与BufferQueue的连接,并

允许其他来连接。


一个给定的线程可以通过改变”current“来在多个EGLSurface之间切换。一个EGLSurface必须是只属于一个线程在某一个时刻的current。


最普遍的错误就是当考虑到EGLSurface时,假设它只是Surface的另一面(像SurfaceHolder)。它是相关但完全独立的概念。你可以在没有Surface支持的EGLSurface上

绘画,也可以在没有EGL下使用Surface。EGLSurface仅仅是给GLES一个地方去画。


ANativeWindow

公共的Surface类是在Java语言下实现的。在C/C++环境下,与之等价的是一个ANativeWindow 类, 部分由Android NDK暴露。你可以调用ANativeWindow_FromSurface函数

来从Surface获得ANativeWindow。就像是它在Java环境下的表弟,你可以对其上锁,在软件方式下绘画,然后解锁并投递。


要从本地化代码中创建一个EGL window surface,你必须传递一个EGLNativeWindowType实例到eglCreateWindowSurface函数那里。EGLNativeWindowType只

是ANativeWindow的同义词,所以你可以随意的cast一方到一方。


事实上,基本的”native window“类型仅仅是对BufferQueue的生产者端的一个包装,不应大惊小怪。


SurfaceView和GLSurfaceView

到此,我们都浏览了底层的构件,是时後看看他们如何适应于app所基于的上层结构。

Android应用UI框架是基于对从View开始的分层。大部分的细节不会影响这里讨论,但有助了解UI元素把自己填进矩形区域所经历复杂的测量和布局过程。当应用被推到

前面时候,所有可视的View对象都渲染到由WindowManager设置的SurfaceFlinger创建的Surface。布局以及渲染都是由应用的UI线程执行的。


不管你有多少个布局和View,所有的事情都渲染到一个简单的缓存。不管View是否硬件加速的,这都是正确的。


SurfaceView采取跟其他View一样的参数,因此你可以给予它一个位置,大小,并给他配上其他元素。当要着色的时候,它内容却完全是透明的。SurfaceView的View部分仅仅

是一个透明的占位符。


当SurfaceView的View部分即将可见时,框架会要求WindowManager去要求SurfaceFlinger创建一个新的Surface。(这不是同步发生的,这就是为什么你必须提供一个回调

函数来通知你Surface创建完成)默认地,新Surface被置于应用app UI Surface之后,但默认的”Z-ordering“可以被覆盖来使Surface置于顶部。


无论你着色什么哦那个西到这个Surface,最终会由SurfaceFlinger合成,而不是由应用app。SurfaceView真正实力是:你获取的Surface可以由分离的线程或分离的进程来着色,与由app UI执行的任何着色动作是隔离的,并且Buffer直接跑到SurfaceFlinger。你不能完全忽视UI线程--你依然要协调Activity的生命周期,还有你也许需要因为大小或位置

的改变而做适当调整---但你自己拥有一整个Surface,由应用appUI来混合,其他图层则交由Hardware Composer完成。


这个新Surface是BufferQueue的生产者,SurfaceFlinger是BufferQueue的消费者,花点时间注意这点是很值得的。你可以用任何可以提供给BufferQueue的机制来更新Surface

。你可以:使用Surface提供的Canvas函数,依附一个EGLSurface和GLES来在其上绘画,并配置一个MediaCodec视频解码器来对它写操作。


Composition 和 Hardware Scaler(合成与硬件缩放)

现在,我们有了更多一点的上下文了,回头看看dumpsys SurfaceFlinger结果中我们之前忽略的几项很有帮助。回到Hardware Composer讨论,我们看下面的输出结果:

     |           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
 
  
这个结果是在Portrait模式下使用Nexus5机器,用GraFika的播放视频来播放视频时候捕抓的。注意,这里的表是按照从后到前的顺序排列的:SurfceView的Surface在后面,
应用的UI层在它之上,接着是状态栏和导航栏在最顶层。视频的分辨率是 QVGA(320X240)。

”source crop"表明了SurfaceFlinger准备显示Surface的Buffer的部分内容。应用app UI被赋予一个和全屏(1080X1920)一样大小的Surface。但被状态栏和导航栏所遮挡的点不会被着色,所以源被裁减到一个从距离顶部75像素开始,到离底部144像素的矩形框内。状态栏和导航栏有比较小的Surface,所以Source Crop描述了一个以左上(0,0)点开始扩展到其内容的矩形。

“frame”是在显示上像素所在的矩形。对于app UI层,frame适配source crop,这是因为我们把显示区域大小的层的一个内容拷贝到另外一个显示区域大小图层的相同位置。
对于状态栏和导航栏,帧矩形框大小是一样的。但内容被调整,以便导航栏可以在屏幕底部显示。

现在考虑标有“SurfaceView”的图层持有我们的视频。因为MediaCodec解码器(buffer生产者)从队列取出该大小的buffer,所以SurfaceFlinger知道source crop适配视频大
小。这帧矩形有完全不一样的大小---984X738

SurfaceFlinger通过缩放buffer的内容来适应帧矩形达到处理大小的区别,如需要,放大或缩小。选择这个大小,是因为这个大小的比例刚好如视频的4:3,并且是在View布局限
制下尽可能宽。(为了美学,还包含了一些在屏幕边缘的填充)

如果你在同一个Surface播放不同的视频,底层的BufferQueue会自动重新申请新大小的缓存,并且SurfaceFlinger会调整source Crop。如果视频方向比例不一样,应用会被迫
去重 布局View来适应它,从而使WindowManager通知SurfaceFlinger更新帧矩形。

如果你通过一些其他方式,假设GLES,来在Surface上着色,你可以用SurfaceHolder#setFixedSize函数来设置Surface大小。你可以,举例,配置一个游戏总在1280X720 上着色 这会明显降低在2560X1440分辨率或4K 电视用于显示的像素数目。显示处理器负责缩放处理。如果不希望游戏变得奇怪,你可以通过设置最小边为720像素,但长边依比例设置来调整游戏的外在比例。你可以看Grafika的“Hardware Scaler Exerciser”的Activity例子。


GLSurfaceView

GLSurfaceView类提供了帮助类来帮助管理EGL上下文,线程内沟通,与Activity生命周期的互动。你并不需要使用GLSurfaceView来使用GLES。

举个例子,GLSurfaceView创建了一个线程来着色以及配置EGL上下文。当activity暂停时,此状态自动清除。大部分应用不需要了解EGL的任何事情才能伴随GLSurfaceView
来使用GLES。

大部分情况下,GLSurfaceView是非常有用的,并且能与GLES工作变得更简单。在某些情况下,它会成为阻碍。如果有用,则用,如果不,则不用。

SurfaceTexture

SurfaceTexure类是一个相对新来者,在Android3.0引入。如SurfaceView是Surface和View的结合,SurfaceTexture差不多是Surface和GLES Texture的结合。

当你创建一个SurfaceTexture,你创建了一个BufferQueue,并且其应用app作为消费端。当新Buffer被生产者入列,你的应用app会由回调函数被通知(onFrameAvailable)。

你的应用app调用updateTextImage来释放之前持有的Buffer,并从队列请求一个新Buffer,并调用EGL的一些函数来使Buffer作为外部texture对于GLES可用。


外部texture(GL_TEXTURE_EXTERNAL_OES)与由GLES(GL_TEXTURE_2D)创建的texture并相当一样。你必须有点区别的配置你的着色器,并且有些事情你不能让它们做

。但关键点是:你可以直接从你BufferQueue接收到的数据来着色纹理化的多边形。


你也许想知道,我们如何可以保证Buffer的数据格式是GLES可以辨别的东西---gralloc 支持广泛的格式。当SurfaceTexture 创建 BufferQueue时,它设置消费者使用标志为

GRALLOC_USAGE_HW_TEXTURE,确保任何由gralloc创建的Buffer对GLES有用。


因为SurfaceTexture与EGL上下文交互,你必须小心地从合适线程去调用它的方法。这个在类文档中被提出。


如果你更深入的阅读类文档,你也许会发现一些奇怪的调用。一个取时间戳,其他取变形矩阵,每一个的值都由前一个调用updateTexImage来设置。这证明了BufferQueue

不单单传递缓存句柄,而是更多的到消费者。每一个缓存都伴随时间戳以及变形参数。


为了效率,提供了转换功能。在一些案例,源数据对于消费者也许是“错误”摆向;但我们不是先旋转它在发送,相反我们可以把数据以目前的摆向并伴随一个修正它的转换来发

送。转换矩阵可以与其他在已用的数据点上的转换融合,这可以减少负载。


时间戳对于特定的Buffer源很有用。举个例子,假设你把摄像头的输出连接到生产者接口(SetPreviewWith)。如果你想创建一个视频,你必须为每一帧设置呈现的时间戳;但

你希望时间戳基于帧被捕获的时间,不是被你应用app接收Buffer的时间。随Buffer提供的时间戳是由摄像头代码设置,导致一系列更加一致的时间戳。


SurfaceTexture和Surface

如果你仔细查看API,你将发现对于应用,只有一个路径来创建一个简单的surface,那就是通过把SurfaceTexture作为唯一构造函数的参数来创建。(API11之前,根本没有Surface的公共构造函数)。如果你把SurfaceTexture视为Surface和Texture的结合,这也许有一点落伍了。


在引擎盖子下面,SurfaceTexture被称为GLConsumer,更准确的反应了其作为BufferQueue的消费者以及拥有者的角色。当你创建来自SurfaceTexture的Surface时,你正在做的事情是创建一个代表SurfaceTexture的BufferQueue生产者一方的对象。


案例学习:Grafika的“Continuous Capture” Activity


摄像头可以提供适合录制电影的帧流。如果你要在屏幕显示,你创建一个SurfaceView,传Surface给setPreviewDisplay,并让生产者(摄像头)与消费者(SurfaceFlinger)

做余下的事情。如果你要录制视频,你调用MediaCodec的createInputSurface创建Surface,传给摄像头,并又一次你可以坐在那里休息一下。如果你同时既要显示视频又要

录制,那你就参与多点。


“Continuous capture” 应用显示来自Camera中正在录制的Video。这个案例,编码的视频被写到内存中的环形Buffer,并可以随时写到磁盘里。只要你跟踪到每一个东西的位

置,那么实现是简单的。

有三个BufferQueue参与。应用app使用SurfaceTexture接收来自摄像头的帧,并转换为一个外部的GLES texture。应用app声明了一个SurfaceView,我们用它来显示帧,并且我

们用一个输入surface配置MediaCodec编码器来创建视频。因此,一个BufferQueue由app创建,一个由SurfaceFlinger,和一个由mediaserver。


图2. Grafika‘s continuous capture activity


在上面的图,箭头显示了来自摄像头的数据传播方向。BufferQueue标以颜色(紫色生产者,蓝绿色消费者)。注意“Camera”实际上是在mediaserver进程。


编好的H.264 视频走向应用app进程中内存的环形buffer,然后当“capture”按钮按下,MediaMuxer类会用来把视频数据以.mp4文件保存到磁盘。

所有的三个BufferQueue由应用app中一个单独的EGL上下文来处理,GLES的操作是在UI线程中执行的。在UI线程中做SurfaceView着色一般是不建议的,但因为我们正在做的

简单操作是由GLES渠道异步处理的,因此这样做应该没问题。(如果视频编码器上锁了且我们屏蔽了尝试出列一个Buffer的动作,应用app将会变得无响应。但在那点上,不管

如何我们可能就失败了)编码数据的处理---管理环形buffer并写到磁盘---是由分离的线程处理的。


大部分的配置是发生在SurfaceView的回调函数 surfaceCreated上的。EGLContext被创建,且EGLSurface也为显示和视频编码器而创建。当新一帧数据到达,我们会告诉

SurfaceTexture去获取它并让它作为GLES texture的存在,接着对每一个EGLSurface用GLES命令来着色(传递来自Surface Texture的变形参数以及时间戳)。编码线程

从MediaCodec拉取编码输出,并存放到内存。


TextureView

TexureView在Android 4.0引进。这是目前这里讨论最复杂的View对象,结合了View和SurfaceTexture。


回忆一下,SurfaceTexture是一个GL消费者,消费着图形数据buffer并让他们作为texture的存在。TextureView封装了SurfaceTexture,接管了响应回调函数以及请求新Buffer的

责任。新Buffer的到来,导致了TextureView产生了一个View刷新请求。当被要求绘画时候,TextureView使用最近接收Buffer的内容作为数据源,并在View状态下指明它在什么地点和什么方式来着色。


你可以用GLES在TextureView如向你在SurfaceView一样着色。只要把SurfaceTexture传递给EGL窗口的创建调用。然而,这样做会暴露潜在问题。


在我们看到的大部分东西里,BufferQueue已经在不同的进程中传递buffer。当使用GLES着色一个TextureView,生产者和消费者都在一个进程,甚至都在一个线程。假设我们

从UI线程连续快速的提交几个Buffers。EGL buffer交换调用将需要从BufferQueue出列一个buffer,它将会停止直到有一个buffer是存在。只有等到消费者请求一个buffer来着色

时候,才会有可以被出列的buffer存在,但这个也是在这个UI线程发生的,因此我们被卡住了。


解决方案就是让BufferQueue保证永远有一个Bufer存在可以被出列,以至于Buffer交换调用不会停止。一个保证这个的方法就是让BufferQueue在新buffer入列时候丢弃之前入列的Buffer的内容,并对最新Buffer数目和最大获取Buffer数目设置限制。(如果你的队列有三个Buffers,所有三个Buffer都已被消费者获取,从而没有Buffer可以出列,缓存的

交换调用就会失败或挂起。因此我们必须防止消费者一次获取超过2个buffers)。丢弃Buffer通常是不可取的,因此只有在特定条件下才会如此,如消费者和生产者在同一个进程中。


SurfaceView或TextureView?

SurfaceView和TextureView都担当类似的角色,但却有非常不同的实现。要决定哪一个是最优,需要对权衡取舍有一定的理解。

因为TextureView是View层级里的一个正常公民,它表现的像其他View,可以覆盖或被其他元素覆盖。你可以使用简单的API调用执行任意的变形和获取以Bitmap格式的内容

的操作。


与TextureView最大的区别是在合成阶段的表现。对于SurfaceView,内容是被写到一个SurfaceFlinger合成好分离的图层,原则上带有覆盖。对于TextureView,View的合成

总是与GLES合成的,并且更新其内容也会引发其他View元素重画(例如,他们被置于TextureView的顶部)。在View着色完成后,应用app UI图层必须由SurfaceFlinger来与

其他图层合成,因此你实际上合成了每个可视像素两次。对于全屏视频播放器,或仅仅作为布局在视频之上的UI元素的任何应用,SurfaceView提供了更好的表现。


如早期注意的,DRM保护视频仅能在一个覆盖面上展现。支持内容保护的视频播放器必须以SurfaceView来实现。


案例学习:Grafika’s 播放视频(TextureView)

Grafika包含了一对视频播放器,一个以TextureView实现,另外一个以SurfaceView实现。视频解码的内容,即从MediaCodec发往一个surface的帧,对两个都是一样的。最

感兴趣的不同实现是要求展现正确纵横比的阶段。


当SurfaceView 要求一个FrameLayout的客制化实现,通过简单配置变形矩阵给TextureView#setTransform函数就可以更改SurfaceTexture的大小。对于前者,你正在通过

WindowManager来发送新窗口位置以及大小给SurfaceFlinger;对于后者,你只要不同方式的着色它。


否则,两个实现方式都遵循一样的模式。一旦Surface被创建,播放被激活。当“play”被按下,视频解码线程就开始,Surface作为输出目标。之后,应用app代码不需要做任何

事,---合成和显示会要么由SurfaceFlinger(对于SurfaceView)或要么由TextureView处理。


案例学习:Grafika‘s 双解码

这个应用演示了在TextureView内部操作SurfaceTexture。

这个应用的基本组织结构是一对TextureView,并排的显示不同的视频。为了模拟一个视频会议应用的需求,我们要在应用因为方向转变而被停止并且被恢复情况下,保持

Mediaodec解码器活跃。技巧就是你不能在没有完全重新配置Surface情况下更改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 帮我们操心渲染,而且它们在UI线程那里执行。


以SurfaceView实现的应用也许会有一点难度。我们不能只是创建一对SurfaceView,并引导输出到它们那里,因为Surface会在应用方向改变时候被销毁。除此之外,那会增加

两个图层,而覆盖层的数量限制会强烈的促使我们要保持图层在最小的数目。因此相反的,我们要创建一对SurfaceTexture来接收来自视频解码器的输出,并在应用执行渲染

工作,使用GLES渲染两个四边形texture到SurfaceView的Surface上。


Conclusion

我们希望这个页面可以提供对于内视Android在系统层面上处理图形的方法有很好的帮助。一些相关话题的信息和建议可以在下面的附录中找到。


附录A:Game Loops

一个很流行的实现游戏循环的方法如下:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until its time to do the next frame
}
 
  这个有一些问题,最基本的想法就是游戏可以定义“frame”是什么。不同的显示会以不同的频率刷新,以及刷新率会变化。如果你生产的帧比显示他们的速度快,你不得不偶 
  

尔丢弃一个。如果你生产的帧太慢,SurfaceFlinger会定期出现不能成功获取一个新缓存,并会重新显示前一帧。这两个情况都会产生显示的问题。


你所需要做的就是匹配好显示帧率,并且依据从前一帧开始所过的时间多少来决定游戏状态的推进。有两个方法来达到如此:1. 把BufferQueue填满,并依靠来回地

“swap buffer”。2. 使用Choreographer(API 16+)


Queue Stuffing(队列填充)

这是很容易实现: 只要能多快有多快的切换Buffer。在Android早期版本,SurfaceView#lockCanvas会让你休眠100ms作为惩罚。现在由BufferQueue来控制步调,并且

BufferQueue可以如SurfaceFlinger一样快速的清空。


这种方法的例子可以在Android Breakout那里看到。它使用GLSurfaceView,调用应用的OnDrawFrame回调然后交换Buffer来运行一个循环。如果BufferQueue是满的,

eglSwapBuffers调用会等待直到有一个Buffer存在。当SurfaceFlinger释放Buffers时候,它们就变得可获取的,一般是在获取一个新Buffer到显示之后。因为这个是在VSYNC

时刻发生的,你的绘画周期时间将会匹配刷新率,大部分的。


这个方法也有一些问题。第一,app被绑在SurfaceFlinger activity,这个将依据需要做的工作多少以及是否需要与其他进程争夺CPU时间来决定需要花费不同的时间。

既然你的游戏状态是依据切换缓存间隔的时间来推进,你的动画就不会在固定的频率刷新。当不是很一致运行在平均60fps下,但你很有可能不会察觉这个碰撞。


第二,第一对Buffer的切换会发生的非常迅速,这是因为此时BufferQueue还不满。帧之间计算的时间几乎接近0,所以游戏会产生一些不产生任何东西的帧。在游戏中如

Breakout,会在每一次刷新时刻去更新屏幕,除了游戏第一次启动,队列总是满的,因此影响不会很明显。游戏经常暂停动画,然后又回到尽可能快的模式,也许会看到奇怪的

打嗝。


Choreographer

Choreographer允许你设立会在下一个VSYNC时刻调用的回调函数。实际的VSYNC时机会作为参数传递。所以即使你应用app没有立刻唤醒,你依然有一个显示开始刷新的

时刻计划。使用则个值而不是当前时间,可以给你游戏状态更新逻辑提供一致的时间源。


不幸的,事实上在每一个VSYNC时刻后你获得一个回调这事并不能保证你的回调被及时的执行或你不能快速的处理它。你的app要检测落后的情况并人为的丢弃一些帧。


在Grafika的“Record GL app” activity提供了这方面的例子。在一些设备上(Nexus 4 和 Nexus 5),如果你只坐在那里看,那么activity将会开始丢弃一些帧。GL 着色虽然很简

单,但View元素常会被重绘,并且测量/布局的传递在设备进入低功耗状态下会花费很长时间。(依据systrace,Android 4.4系统上,时钟变慢后它花费28ms而不是6ms。

如果你在屏幕上拖动你的手指,设备人为你在于activity交互,因此时钟会加速到比较高,你就从不会抛弃一帧)


简单的补救就是在VSYNC时刻后如果当前时间超过N微妙,Choreographer回调就简单的丢弃一帧。理想的N值是基于之前观察的VSYNC的间隔。举个例子,如果刷新周期在

16.7ms(60fps),如果你运行的滞后超过15ms,你也许会丢弃一帧。


如果你观察“Record GL app”运行,你会看到丢弃帧计数器在增加,甚至可以在丢弃帧时候看到边界在闪红色。如果你的眼睛不是特别好,那么你可能看不到动画的断续。在

60fps速度下,应用app会经常丢弃帧,只要动画连续的以固定速度进行,则没有任何人会注意这点。你能抛弃多少,在某程度上这是由你画的内容,显示的特性以及使用app

的人对于闪动有多敏感来决定。


Thread Management

一般来说,如果你在SurfaceView,GLSurfaceView,或 TextureView上着色,你要在指定的线程上进行。绝对不要在UI线程上做重活或一些不确定时间的事情上。

Breakout和“Record GL app” 使用指定的着色线程, 并且他们也在这个线程上更新动画。只要游戏状态能迅速的刷新,这是一个很合理的方法。

其他游戏完全的把游戏逻辑和游戏着色分开。如果你有一个简单的游戏,仅仅是每100毫秒移动一个块,而不用做别的,你可以有一个指定的线程仅仅做如下:

run() {
        Thread.sleep(100);
        synchronized (mLock) {
            moveBlock();
        }
    }
 
  (你也许需要以睡眠时间为基础的固定时钟来防止漂移----sleep() 不是很一致, moveBlock() 接受一个非零时间---但你懂的) 
  


当绘画代码唤醒,它只是抓住锁,获取当前位置的块,释放锁并开始绘画。与其基于inter-frame的delta时间来做分级运动,你只有有一个线程移动事物,另外一个线程在绘画

开始后绘画出现在任何地方的东西。

对于拥有任何复杂度的场景,你会需要创建一个由唤醒时间排序的即将到来的事件列表,并休眠直到下一个事件到达,但这是一样的思想。


附录B: SurfaceView 和 Activity 生命周期

当使用SurfaceView时候,从其他线程而不是UI线程来着色Surface被认为是很实际的做法。这引起了一个关于其他线程与Activity生命周期交互的问题。


1. Application onCreate / onResume / onPause

2. Surface create / changed / destroyed


当Activity开始,你的回调按照以下次序:

  • onCreate
  • onReusme
  • surfaceCreated
  • surfaceChanged
如果你按“back”,你有:
  • onPause
  • surfaceDestroyed (在Surface即将消失时候调用)
如果你旋转屏幕,Activity被拆除然后重建,因此你获得一个完整周期。如果这个很重要,你可以说,这是一次通过检测 isFinishing()函数来快速重启。(很有可能开始或停止
Activity太快,导致SurfaceCreate() 也许实际上只在onPause()发生。

如果你按下power键来灭屏,你只能得到onPause()---没有 surfaceDestroyed()。Surface要保持激活,着色才可以进行。你甚至能保持获取Choreographer事件,只要你继续请求。如果你有一个锁屏会强制不同方向,你的activity也许重新开始当设备点亮时候。但如果没有,你会得到一个你之前拥有的surface的黑屏。

这里引起了基本的问题,当对SurfaceView使用分离的着色线程:线程的生命周期要联系到activity还是surface?答案依赖于你希望在屏灭时候发生的事情而定。有两个基本的
方式:1. 在Activity的开始/停止 那里 开始/停止线程。 2. 在Surface的创建/销毁那里开始/停止线程。

#1 与app生命周期交互的很好。我们在onResume那里启动着色线程,并在onPause那里停止它。当创建和配置这个线程时,这方法有点笨拙。因为某些时候,Surface已经
存在,有些时候,它不存在(例子,它在按下power键使屏幕开关时刻依然处于激活)。我们不得不等待surface被创建后才能配置和初始化线程,但我们不能简单的在
surfaceCreate回调函数里来做,因为如果Surface并没有重新创建,这个回调不会被调用。因此我们需要询问或缓存Surface的状态,并送到着色进程那里。只,我们必须要
小心在线程间传递对象----最好通过消息处理器来传递Surface或SurfaceHolder,而不是直接填进线程里,以求避免在多核系统中发生什么(Android SMP Prime)

#2 有一定的吸引力,因为Surface和着色器逻辑上是互联的。我们在Surface创建后启动线程,避免了对一些线程间通信的关注。Surface 创建/改变消息简单的投递。我们需
要确保当屏幕熄灭时候,着色停止,在屏幕亮起时恢复;让Choreographer停止调用帧绘画回调是一个简单的事情。我们的onResume函数会需要恢复回调函数,仅当着色线
程在运行。这也许没那么简单---如果我们基于帧间消逝的时间来做动画,我们可以在下一个事件到来时,有非常大的间隙;因此,一个显示的暂停/恢复 消息也许是合要求
的。


上述都是主要关注着色线程如何配置以及是否它是运行的。一个相关的关注是提取当activity被杀死时,来自线程的状态。(in onPuase 或 onScreenInstantceState), 方法

 #1 会比较好,因为一旦着色线程被连接上,他的状态可以不需要同步原语就能访问。

你可以看方法#2 在Grafik的“Hardware scaler exercise”的例子


附录C:使用systrace跟踪BufferQueue

如果你真的需要了解图像Buffer如何周围移动,你需要使用systrace。系统层的图形代码结构化很好,如大多数相关的应用框架代码一样。使能“gfx”和“view”标签,同时一般的

“sched"。

对如何高效使用systrace的全部描述会填充相当长的文档。一个显著的项就是在trace中出现的BufferQueue。如果你之前用过systrace,你可能会看到他们,但却不肯定他们是

什么。作为例子,如果你在Grafik的”play video“跑的时候抓到一份trace,你可以看到一行标有”SurfaceView“。这行告诉你在给定时刻有多少个buffer在队列中。


当app激活时,你会注意到这个值在增加---触发Mediacoder解码器渲染帧-----以及当SurfaceFlinger工作时候,消费Buffer时候,这个值在减少。如果你以30fps显示视频,队列

的值会从0到1变化,因为60fps频率的显示可以很容易的跟上源。(你也将注意到SurfaceFlinger仅仅在有事情做的时候才唤醒,并不是60次没秒。系统尽量避免工作,在没有

更新屏幕时刻会完全停止VSYNC)


如果你打开”play video“并抓到一份新trace,你会看到一行有很长名字的的一行(com.android.grafika/com.android.grafika.PlayMovieActivity"),这是主UI图层,无疑这是另一

个BufferQueue。因为TextureView着色到UI图层,而不是另外一个图层,你会在这里看到所有的视频驱动的更新。



你可能感兴趣的:(OpenGL-ES,Android)