转载地址 : 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一致或完全不能在用户空间访问。创建的本质是由使用标志觉定的,
如下面属性:
某些值不能在特定的平台上被组合。举个例子,”视频编码器“ 标志也许需要 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如下工作:
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)处理,以及其他一堆你大概不关心的数据(句柄,提示,标志以及其他我们
为了避免存在显示的撕裂情况,系统需要双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的流程。
与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 it’s 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开始,你的回调按照以下次序:
上述都是主要关注着色线程如何配置以及是否它是运行的。一个相关的关注是提取当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图层,而不是另外一个图层,你会在这里看到所有的视频驱动的更新。