Android Graphics Architecture翻译(上)

来源:互联网 发布:excel03显示重复数据 编辑:程序博客网 时间:2024/06/05 12:42
原文地址https://source.android.com/devices/graphics/architecture.html
墙内链接http://blog.csdn.net/kastland/article/details/40016965


关于Surface、SurfaceHolder、EGLSurface、SurfaceView、GLSurfaceView、SurfaceTexture、TextureView、和SurfaceFling,每一个开发者都应该知道的东西。

这篇文档讲述了Android系统级图像架构的一些必备元素以及它们是如何应用于应用程序框架和多媒体系统中的。了解这些的关键是掌握图像的缓冲数据是如何在系统中传递的。如果你想知道SurfaceView和TextureView是如何工作的,或者Surface和EGLSurface是如何交互的,你可以从这篇文章中得到答案。

我们假设你熟悉一些Android设备以及应用程序开发技术。你不需要对应用程序框架有太多深入的了解,而且我们很少会涉及到具体的API调用,但是这篇文档所讲的东西不会重复过多现有的公开文档。我们的目标是让你对渲染架构的几个重要环节有一个大体的认识,这样你可以在设计自己的app时做出更明智选择。为此我们将采用从底向上的方法,更多的讲述这些UI相关的类是如何工作的而不是他们是如何使用的。

前面的章节包含了一些在后面章节用到的背景材料,所以你最好从前往后通读全篇而不是凭兴趣跳过一些章节。我们将从Android的图像缓冲开始,讲述它的合成和显示机制,然后再到更高级的数据合成器的机制。

这篇文档主要基于Android4.4系统。早期的版本工作原理是不同的,未来的版本也将会有区别。这些不同点在有些地方会被强调出来。

BufferQueue和gralloc

要了解Android图像系统如何工作,我们必须从这一切的背后说起。Android图像系统的核心是一个叫做BufferQueue的类。他扮演的角色很简单:连接那些生产图像数据的一方(生产者)和接收数据用于显示或者进一步处理的一方(消费者)。生产者和消费者可以是在不同进程中。几乎所有图像数据缓冲的传输依赖于BufferQueue。

它最基本的用法也是很简单的。生产者请求一个空闲的buffer(dequeueBuffer()),制定一系列的参数例如width、height、pixel format和usage flags。生产者填充这个缓冲然后返回给BufferQueue(queueBuffer())。之后,消费者 获取这块buffer(acquireBuffer()),然后对buffer内容加以利用。当消费者昨晚之后,再把这块buffer还给BufferQueue(releaseBuffer())。

最近Android设备支持了“同步框架“,配合那些可以同步操作图像数据的硬件,系统可以完成一些漂亮任务。举个例子,生产者可以一下子提交一系列的OpenGL ES 绘制指令然后在绘制完成之前就将缓冲区交付给消费者。这块buffer会被分配一个栅栏信号用于通知内容绘制完成的时机。当这个buffer被退回到空闲列表的时候,尽管可能还没有被使用完毕,它也会被分配一个这样的栅栏,不过它是用于通知内容已经消费者被使用完毕了,这样消费者就可以在它的内容还在使用的时候释放这块buffer。这项优化可以在buffer传递过程中降低延迟和提升吞吐量。

BufferQueue的一些参数,例如buffer的最大值,是由生产者和消费者联合决定的。

BufferQueue只有在需要buffer的时候才会分配buffer。Buffer除非参数改变不会被销毁。例如,如果生产者请求了一个不同大小的buffer,原来的buffer将会被释放,新buffer将会按需分配。

BufferQueue是由消费者生产和持有的。在Android 4.3只有生产者是binder化的,例如生产者可以是在远程进程里但是消费者不许在queue创建的进程里。4.4朝着更普遍的情况进化了一点。

Buffer的内容数据永远不会被BufferQueue复制。传输这样的数据非常低效。因此,Buffer的传递是通过handle进行的。

gralloc HAL
实际上buffer分配是由一个专门的叫做gralloc的内存分配器来完成的。后者实现了厂商定制的HAL接口。alloc函数需要你期望的一些参数,width、height、pixel fromat和usage flags。flags可能需要好好看看。

gralloc分配器并不是在native堆里分配内存的另一种方式。在某些情况下,被分配了的内存必须具有缓存一致性,不然整块都将在用户空降无法访问。分配的本质是由usageflags决定的,包括以下几个:

CPU处理这块内存的频率怎么样
GPU处理这块内存的频率如何
这块内存是否将被用来作为一张OpenGL ES纹理。
这块缓存是否将被用于视频编码。

例如,如果你指明了一块RGBA 8888的buffer,你期望这块buffer通过软件模拟绘制--这意味着你的程序将直接和像素打交道。之后你的分配器需要创建一个4byte像素R_G_B_A顺序的buffer。如果你说这块buffer只能被硬件作为GLES纹理来使用,分配器可以做任何GLES驱动想做的事情---例如BGRA顺序、非线性的交叉混合的排版等等。让硬件使用它最喜欢的像素格式以便提高性能。

一些app无法做到所有平台都能玩。例如视频编码器标志可能需要YUV格式的像素,所以这时添加”software access“并且指定RGBA8888的像素格式将会失败。

gralloc分配器返回的handle可以通过binder在进程间传递。

SurfaceFlinger和Hardware Composer

有了图像数据的buffer并不能让我们满足,我们需要看到他们是如何显示在屏幕上的。这时SurfaceFlinger和Hardware Composer HAL就登场了。

SurfaceFlinger的使命是接受不同来源的图像数据buffer,加以合成,输送到显示屏上。过去这些操作是通过软件方式复制到硬件framebuffer上的,现在已经不这样做了。

当一个app被切换到前台来的时候,WindowManagerService请求SurfaceFlinger绘制surface。SurfaceFlinger创建一张“layer"--其实就是一个BufferQueue--由SurfaceFlinger扮演消费者的角色。WIndowManagerService会返回给生产者app一个Binder对象,之后就可以通过它来向SurfaceFlinger直接输送帧数据了。(注意:在这里WindowManager 用术语”window“而不是”layer“,”layer“别有他用。实际上SurfaceFlinger如果叫LayerFlinger会更好一点。

对于大多数app,屏幕上同时会有3个layer:最上面的状态栏,下面的导航条以及app自己的UI。一些app也有可能多或少,例如默认的桌面有一个单独的layer专门用来承载壁纸,又例如一些全屏的游戏可以把上面的状态栏隐藏掉。每一个layer可以独立更新。状态栏和导航条是由system process渲染的,而app的layer是由app自己渲染的,它们之间没有合作关系。

显示屏往往是定时刷新的,手机和平板上通常是每秒60帧。如果屏幕的显示内容在两次刷新中间更新的话,我们就会看到”撕裂“的现象。所以有规律的更新显示内容是非常重要的。系统会接受来自显示屏的刷新信号来更新自己的显示内容,我们称之为垂直同步信号。

刷新率可能随时会变,例如一些手机可能从58帧到62帧每秒变化。对于HDMI接入的电视,这个值就是24或者46HZ。因为我们只能在一次刷新周期里更新屏幕,所以太过频繁的提交buffer甚至达到200fps实际上是一个浪费,多出来的帧也不会被显示。Surfaceflinger只有在有新数据到来的时候才会醒来而不是一有数据就会工作.

当垂直同步的信号到来的时候,SurfaceFlinger遍历它的layer列表寻找新的buffer。如果有新的,它将获取这块buffer;如果没有,它会继续使用之前获取的buffer。SurfaceFlinger总是需要显示东西的,所以它会一直持有一块buffer。如果一个layer中还没有buffer提交的话,它将被SurfaceFlinger忽略。

Hardware Composer
Hardware Composer HAL("HWC")是3.0首次引入的,之后越来越稳定。它最主要的目的是利用可用的硬件用最有效的方式去合成buffer。作为一个HAL层,它的实现是设备相关的,也是通常由OEM厂商来实现的。

HWC特别适合处理"覆盖层Overlay”。Overlay的目的是利用显示硬件取代GPU去把多个buffer合成在一起。例如,假设你有一个典型的竖屏的Android手机,上面有状态栏,下面有导航条,app占据中间。每一个layer的显示内容在不同的buffer。你可以再找一块临时的buffer,把app显示内容贴上去,然后把状态栏贴上去,再把导航条贴上去,最终把这块临时buffer输送到显示硬件中,也可以换一种思路,直接把三块buffer输送到显示硬件,只需要告诉他们buffer的内容在屏幕中的不同位置。显然后者更加有效。

你可能会想到,不同的显示处理器可能处理能力有很大不同,overlay的数目、它们是否可以旋转或者混合以及定位和重叠的限制等问题很难通过一个API完全搞定。所以HWC的工作流程是这样的:

1、SurfaceFlinger给HWC提供所有的layer列表,然后询问HWC如何处理这些layer
2、HWC之后会把每个layer标记为”GLES的Layer“或者”Overlay“
3、SurfaceFlinger然后处理那些被标记为GLESLayer的layer,然后把输出发送给HWC,HWC处理剩下的layer。

第二部中标记layer的代码是硬件供应商来决定的,所以它可以发挥硬件的最佳性能。

当屏幕的显示内容没有任何变化的时候,GL合成可能比Overlay更快速,尤其是在Overlay的内容有透明像素的时候或者重叠的layer相互混合的时候。在这些情况下,HWC可以选择请求GLES 合成器来合成所有的或者一部分这样的layer,然后把合成的临时layer发回给它。如果SurfaceFlinger回头再来请求合成这些layer,那HWC可以把上次合成的临时layer直接返回给它,这样可以节省很多电量。

4.4系统的设备通常支持4个Overlay。当HWC同时处理超过4个overlay的时候系统会自动切换到GLES合成器,因此应用程序使用的layer越多,对于电量的消耗和性能的影响就越大。

你可以用adb shell dumpsys SurfaceFlinger看到SurfaceFlinger的状态。请看下图:

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
从上面我们可以看到屏幕上所有的layer,以及他们是被标记为Overlay(由HWC处理)还是OpenGL ES 来合成(”GLES“)。上图还有一些你可能不太关心的参数(”handle“、”hints“、”flags“以及一些我们没有标出的其他信息。)“source crop”和“frame”下面就会讲到。

标记为FB_TARGET的layer就是GLES合成的输出。因为上面所有的layer都是Overlay,FB_TARGET在这一帧没有被使用。从它的名字里我们就知道他扮演的角色:在一个有/dev/graphics/fb0没有Overlay的设备上,所有的合成都是用GLES做的,输出将被写入framebuffer。最近的很多设备已经美哟framebuffer了,所以FB_TARGET layer实际上就是一块临时buffer。(注意:这也是为什么屏幕录像软件在最近的Android设备上不能使用的原因:因为它们通常是直接从Framebuffer里读取数据了,现在已经没有framebuffer了。)


Overlay还有一个很重要的作用:它是显示DRM内容的唯一途径。DRM保护的buffer是不能被SurfaceFlinger或者GLES驱动操作的,这也就意味着如果HWC切换到GLES合成器的时候,你的视频会播放不出来。


三缓冲区的必要性


为了防止“撕裂”现象,系统需要支持双缓冲:前台buffer在显示的同时后台buffer准备数据。垂直同步信号到来的时候,如果后台buffer准备好了,你可以切换二者。如果你的绘制结果直接输送到framebuffer的话,这样做是行得通的。但是如果再加上一个合成的步骤就不行了。正式因为SurfaceFlinger的工作方式,使得传统的双buffer流水线遇到了困难。

假设frame N正在显示,frame N+1已经被SurfaceFlinger锁定用于下个垂直同步信号显示。(假设frame N是通过Overlay的方式合成的,所以我们不能在显示器使用完它之前把它替换掉。)当垂直同步信号到来的时候,HWC切换了buffer。这时app开始渲染frame N+2的内容到那块曾经承载frame N内容的buffer上,同时SurfaceFlinger遍历它的layer列表,寻找有更新的layer。很明显SurfaceFlinger不会找到新的buffer(因为app还没有渲染完,相当于这个16ms里SurfaceFlinger是空闲的,因为它在等待app渲染结束。)所以它只能在下一个垂直同步信号到来的时候继续显示frame N+1(也许这是frameN+2已经被app渲染完了,但是SurfaceFlinger还没有对它进行合成,所以不能用于显示)。过了一段时间,app渲染完了frame N+2然后交付给Surfaceflinger,但是已经太晚了。最坏的情况下这样会导致我们的最大帧率减半。

三buffer可以弥补。在垂直同步信号到来的之前,frameN正在被现实,frameN+1已经被合成结束(或者overlay准备就绪)等待被显示了,frameN+2也已经被SurfaceFlinger锁定用于合成。当屏幕刷新之后,所有的buffer按顺序进入下一阶段。app只是延迟了一个垂直同步周期(16ms)来渲染它自己的buffer。SurfaceFlinger/HWC在下一次刷新之前有一个垂直同步周期(16ms)的时间来完成他的合成工作。缺点就是app产生的内容至少要经过两个垂直同步周期才能显示到屏幕上。延迟增加了,你会感觉屏幕滑起来不那么跟手了。



上面的时序图描述了SurfaceFlinger和BufferQueue的工作流程。在这一帧的时间里:
1、红色buffer将会被填满,然后输送到BufferQueue中
2、红色buffer离开app之后,蓝色buffer进入,取代它的位置。
3、绿色buffer和SystemUI*(灰色的状态栏)进入HWC(这里显示SurfaceFlinger依然持有这块buffer,但是现在HWC已经准备好在下一个垂直同步信号到来的时候输送它们到屏幕了。

实际上蓝色buffer同时被屏幕和BufferQueue引用。app在它身上的同步栅栏唤醒之前不允许使用这块buffer。

垂直同步信号到来的时候,下面的将同时发生:

红色buffer进入SurfaceFlinger,取代绿色Buffer。
绿色buffer进入屏幕,取代蓝色buffer。绿色buffer同时出现在BufferQueue和屏幕上。
蓝色buffer的栅栏信号唤醒,然后被清空**,表明app可以使用蓝色buffer了
显示屏从绿色变成了蓝色。

注释:
*-SystemUI进程用于提供状态栏和导航栏,所以我们认为他们没有改变。所以SurfaceFlinger继续使用上一次锁定的buufer。事实上将会有两个不同的buffer,一个上面的Statusbar,一个是下面的导航栏。他们的大小和自己的内容一样大。每一块都会保存在他们自己的BufferQueue里。
**-buffer并不是真正的被清空了。如果你没有画任何东西就交付了它,你可能会得到原来的蓝色。清空只有在app画其他内容的时候才会做这样的操作。

我们可以通过让layer的合成器不要一下锁定一块buffer一整个的垂直同步周期(16ms)来减少延迟。如果用overlay来合成,它可能不花CPU时间和GPU时间。但是我们不能这样算,还是要留出一点点时间来。如果app在垂直同步周期(16ms)的中间开始渲染,SurfaceFlinger延迟HWC启动工作到信号到来的前几毫秒的话,我们可以把延迟从2帧减少到1.5帧(这个要求app和SurfaceFlinger在VSYNC到来时错开一段时间启动工作,4.4加入的)。理论上你可以在同一个周期里完成渲染和合成工作,但是现有的设备很难做到这一点,一些渲染和合成时间上的小波动,或者overlayer切换到GLES合成的耗时,都有可能使我们丢掉一个交换缓冲区的时机而只能重复显示上一帧的内容。

SurfaceFlinger的合成buffer掩饰已经有栅栏信号的buffer管理上面就介绍完了。如果我们想满速率跑动画的话,我们就得有一个被锁定的显示buffer(前台buffer)和一个被锁定的绘制buffer(后台buffer)。如果我们在overlay上显示buffer的内容的时候,内容必须可以直接被现实屏直接获取并且不能中途修改。但是如果你看上面图片里面的layer状态,你会看到只有一个被锁定的buffer,一个入队的buffer和一个空闲的buffer。这事因为,当SurfaceFlinger锁定了一块新的后台buffer,它会释放当前的前台buffer到bufferqueue里。前台buffer已然在被屏幕使用,所以任何使用这块buffer的app必须等到栅栏信号唤醒才能绘制内容到上面。只有人人遵守这个规则,这些queue操作的IPC请求才能在显示buffer的同时发生。
0 0
原创粉丝点击