Android
图形 Graphic
和显示 Display
是两个独立的部分,这里放在一起简述;介绍了图像和显示相关的基本概念,比如 BufferQueue
生产者消费者模型, Surface/SurfaceFlinger
图形合成等等。
概念
应用开发者可通过两种方式将图像绘制到屏幕上:使用 Canvas
或 OpenGL
:
android.graphics.Canvas
是一个2D
图形API
,Canvas API
通过一个名为OpenGLRenderer
的绘制库实现硬件加速,该绘制库将Canvas
运算转换为OpenGL
运算,以便它们可以在GPU
上执行。从Android 4.0
开始,硬件加速的Canvas
默认情况下处于启用状态- 除了
Canvas
,开发者渲染图形的另一个主要方式是使用OpenGL ES
直接渲染到Surface
。Android
在Android.opengl
软件包中提供了OpenGL ES
接口
EGL
先熟悉 Android
平台图形处理 API
的标准:
OpenGL
是由SGI
公司开发的一套3D
图形软件接口标准,由于具有体系结构简单合理、使用方便、与操作平台无关等优点,OpenGL
迅速成为3D
图形接口的工业标准,并陆续在各种平台上得以实现。OpenGL ES
是由khronos
组织根据手持及移动平台的特点,对OpenGL 3D
图形API
标准进行裁剪定制而形成的。Vulkan
是由khronos
组织在 2016 年正式发布的,是OpenGL ES
的继任者。API
是轻量级、更贴近底层硬件close-to-the-metal
的接口,可使GPU
驱动软件运用多核与多线程CPU
性能。
OpenGL ES
定义了一个渲染图形的 API
,但没有定义窗口系统。为了让它能够适合各种平台,它将与知道如何通过操作系统创建和访问窗口的库结合使用。而在 Android
中,这个库被称为 EGL
;也就是说 EGL
主要是适配系统和关联窗口属性。如果要绘制纹理多边形,应使用 OpenGL ES
调用;如果要在屏幕上进行渲染,应使用 EGL
调用。OpenGL ES
是 Android
绘图 API
,但 OpenGL ES
是平台通用的,在特定设备上使用需要一个中间层做适配, Android
中这个中间层就是 EGL
。
Surface
和 SurfaceFlinger
无论开发者使用什么渲染 API
,一切内容都会渲染到 Surface
。 Surface
表示缓冲队列中的生产者,而缓冲队列通常会被 SurfaceFlinger
消耗。在 Android
平台上创建的每个窗口都由 Surface
提供支持。所有被渲染的可见 Surface
都被 SurfaceFlinger
合成到显示部分。它们遵循生产者/消费者模型:
- 图像流生产者
图像流生产者可以是生成图形缓冲区以供消耗的任何内容。例如OpenGL ES, Canvas 2D, mediaserver
视频解码器。 - 图像流消费者
图像流的最常见消费者是SurfaceFlinger
,该系统服务会消耗当前可见的Surface
,并使用窗口管理器中提供的信息将它们合成到显示部分。SurfaceFlinger
是可以修改所显示部分内容的唯一服务。SurfaceFlinger
使用OpenGL
和Hardware Composer
来合成一组Surface
。
其他OpenGL ES
应用也可以消耗图像流,例如相机应用会消耗相机预览图像流。非GL
应用也可以是消费者,例如ImageReader
类。
WMS: WindowManagerServices
窗口管理器,控制窗口的 Android
系统服务,它是视图容器。窗口总是由 Surface
提供支持。该服务会监督生命周期、输入和聚焦事件、屏幕方向、转换、动画、位置、变形、 Z-Order
以及窗口的其他许多方面。窗口管理器会将所有窗口元数据发送到 SurfaceFlinger
,以便 SurfaceFlinger
可以使用该数据在显示部分合成 Surface
。
FrameBuffer
FrameBuffer
帧缓冲驱动,它是 Linux
的一种驱动程序接口。 Linux
是工作在保护模式下,所以用户态进程是无法象 DOS
那样使用显卡 BIOS
里提供的中断调用来实现直接写屏, Linux
抽象出 FrameBuffer
这个设备来供用户态进程实现直接写屏。 FrameBuffer
机制模仿显卡的功能,将显卡硬件结构抽象掉,可以通过 FrameBuffer
的读写直接对显存进行操作。用户可以将 FrameBuffer
看成是显示内存的一个映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反应在屏幕上。这种操作是抽象的统一的。用户不必关心物理显存的位置、换页机制等等具体细节,这些都是由 FrameBuffer
设备驱动来完成的。但 FrameBuffer
本身不具备任何运算数据的能力,就只好比是一个暂时存放水的水池。 CPU
将运算后的结果放到这个水池,水池再将结果流到显示器,中间不会对数据做处理。应用程序也可以直接读写这个水池的内容在这种机制下,尽管 FrameBuffer
需要真正的显卡驱动的支持,但所有显示任务都有 CPU
完成,因此 CPU
负担很重。
在开发者看来, FrameBuffer
本质上是一块显示缓存,往显示缓存中写入特定格式的数据就意味着向屏幕输出内容。所以说 FrameBuffer
就是一块白板。例如对于初始化为 16 位色的 FrameBuffer
来说, FrameBuffer
中的两个字节代表屏幕上一个点,从上到下,从左至右,屏幕位置与内存地址是顺序的线性关系。
帧缓存可以在系统存储器(内存)的任意位置,视频控制器通过访问帧缓存来刷新屏幕。帧缓存也叫刷新缓存 FrameBuffer
或 RefreshBuffer
,这里的帧 Frame
是指整个屏幕范围。帧缓存有个地址,是在内存里。我们通过不停的向 FrameBuffer
中写入数据,显示控制器就自动的从 FrameBuffer
中取数据并显示出来。全部的图形都共享内存中同一个帧缓存。
FrameBuffer
帧缓冲实际上包括两个不同的方面:
Frame
:帧,就是指一幅图像,在屏幕上看到的那幅图像就是一帧Buffer
:缓冲,就是一段存储区域,可这个区域存储的是帧
FrameBuffer
就是一个存储图形/图像帧数据的缓冲。Linux
内核提供了统一的 Framebuffer
显示驱动,设备节点 /dev/graphics/fb*
或者 /dev/fb*
,以 fb0
表示第一个 Monitor
,当前实现中只用到了一个显示屏。这个虚拟设备将不同硬件厂商实现的真实设备统一在一个框架下,这样应用层就可以通过标准的接口进行图形/图像的输入和输出了:
从上图中可以看出,应用层通过标准的 ioctl, mmap
等系统调用,就可以操作显示设备,用起来非常方便。这里 mmap
把设备中的显存映射到用户空间的,在这块缓冲上写数据,就相当于在屏幕上绘画。
Gralloc
Gralloc
的含义为是 Graphics Alloc
图形分配 。 Android
系统在硬件抽象层中提供了一个 Gralloc
模块,封装了对 Framebuffer
的所有访问操作。Gralloc
模块符合 Android
标准的 HAL
架构设计;它分为 fb
和 gralloc
两个设备:前者负责打开内核中的 Framebuffer
、初始化配置,以及提供 post, setSwapInterval
等操作;后者则管理帧缓冲区的分配和释放。上层只能通过 Gralloc
访问帧缓冲区,这样一来就实现了有序的封装保护。
Gralloc
图形内存分配器,分配图像生产者请求的内存。它不仅仅是在原生堆上分配内存的另一种方法;在某些情况下,分配的内存可能并非缓存一致,或者可能完全无法从用户空间访问。分配的性质由用法标记确定,这些标记包括以下属性:
- 从软件
CPU
访问内存的频率 - 从硬件
GPU
访问内存的频率 - 是否将内存用作
OpenGL ES: GLES
纹理 - 视频编码器是否会使用内存
例如如果格式指定为 RGBA 8888
像素,并且指明将从软件访问缓冲区(这意味着应用将直接触摸像素),则分配器必须按照 R-G-B-A
的顺序为每个像素创建 4 个字节的缓冲区。相反如果指明仅从硬件访问缓冲区且缓冲区作为 GLES
纹理,则分配器可以执行 GLES
驱动程序所需的任何操作 - BGRA
排序、非线性搅和布局、替代颜色格式等。允许硬件使用其首选格式可以提高性能。某些值在特定平台上无法组合,例如视频编码器标记可能需要 YUV
像素,因此将无法添加软件访问权并指定 RGBA 8888
。
Gralloc
分配器返回的句柄可以通过 Binder
在进程之间传递。
HWC
HWC: Hardware Composer
硬件混合渲染器,显示子系统的硬件抽象实现。 SurfaceFlinger
可以将某些合成工作委托给 Hardware Composer
,以分担 OpenGL
和 GPU
上的工作量。 SurfaceFlinger
只是充当另一个 OpenGL ES
客户端。因此在 SurfaceFlinger
将一个或两个缓冲区合成到第三个缓冲区中的过程中,它会使用 OpenGL ES
。这样使合成的功耗比通过 GPU
执行所有计算更低。Hardware Composer HAL
则进行另一半的工作,并且是所有 Android
图形渲染的核心。 Hardware Composer
必须支持事件,其中之一是 VSYNC
(另一个是支持即插即用 HDMI
的热插拔 hotplug
) 。
VSYNC 垂直刷新
先介绍几个概念:
- 帧
视频,动画中的每一张画面,而视频和动画特效就是由无数张画面组合而成,每一张画面都是一帧。 - 帧率
Frame Rate
,也指帧速率,单位fps:frames per second
,描述视频、电子绘图或游戏每秒播放多少帧;FPS
是测量用于保存、显示动态视频的信息数量;我们本篇讲的帧率/帧速率指的是系统显卡处理的速率,即每秒能处理多少帧。
每秒钟帧数愈多,所显示的动作就会愈流畅。通常要避免动作不流畅的最低是 30 ,所以常见的有30fps, 60fps
等。wiki
百科中提到几个帧率数字:12 fps
:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约 10-12 帧的时候,就会认为是连贯的,是动画的24 fps
:有声电影的拍摄及播放帧率均为每秒 24 帧,对一般人而言已算可接受30 fps
:早期的高动态电子游戏,帧率少于每秒 30 帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低60 fps
:在实际体验中,60 帧相对于 30 帧有着更好的体验85 fps
:一般而言,大脑处理视频的极限
- 屏幕刷新频率
Refresh Rate
或者是Scanning Frequency
,单位赫兹Hz
(即周期/秒),是指设备刷新屏幕的频率,通常为60hz
。屏幕的刷新过程是每一行从左到右(行刷新,水平刷新,Horizontal Scanning
),从上到下(屏幕刷新,垂直刷新,Vertical Scanning
)。 Tearing
撕裂
显示出来的图像出现上半部分和下半部分不属于同一帧,我们称之为tearing
。假设系统显卡处理能力为FPS 100
,显示器的刷新频率是75Hz
,显卡将比显示器快1/3
;这意味着,在一个刷新周期之内,显卡将写入4/3
的帧数据,也就是说下一帧的 1/3 覆盖在前一帧之上;当然随着系统运行, 1/3 这个比例会发生变化,1/3,2/3,1,1/3,循环;这种帧与帧之间的不完全覆盖重合现象就是Tearing
撕裂现象。
显卡处理图像的帧速率和屏幕刷新频率是相互独立的,当两者不一致时会出现 tearing
问题,为了解决不一致的问题,引入了 Vsync
信号:当整个屏幕刷新完毕,即一个垂直刷新周期完成,会有短暂的空白期,等待定期同步信号 VSync
信号,收到后才开始下一次屏幕刷新;所以 VSync
中的 V
指的是垂直刷新中的垂直 Vertical
。Vsync
技术意味着,显卡显示性能极限被限制在屏幕刷新率以内了:在系统显卡处理的 FPS
高于屏幕刷新率时,显卡会将一部分时间浪费在等待上;因为没有可用的内存用于绘制,显卡需要等待 Vsync
信号才能绘制下一帧。
单缓存缓存模型
理想的情况是帧率和刷新频率相等,每绘制一帧,屏幕显示一帧,如下图所示;但是如果不一致,就会出现tearing
。双重缓存
Double Buffer
两个缓存区分别为Back Buffer
和Frame Buffer
。GPU
向Back Buffer
中写数据,屏幕从Frame Buffer
中读数据。当屏幕刷新完成后产生VSync
信号,此时将数据从Back Buffer
复制到Frame Buffer
,可认为该复制操作在瞬间完成;复制完后显示设备开始显示这帧数据,同时通知CPU/GPU
绘制下一帧图像。
但是当GPU/CPU
绘制一帧的时间超过了Vsync
时,屏幕刷新从Frame Buffer
取到的数据仍然是上一帧数据,即两个Vsync
周期显示同一帧数据,我们称为发生了掉帧Dropped Frame, Skipped Frame, Jank
现象。三重缓存
Triple Buffer
在双重缓存模型中,当Jank
现象出现时,GPU/CPU
此时都处于闲置状态,所以引入了三重缓存的概念:在Jank
时,GPU/CPU
在第三个Buffer
中绘制数据:
需要注意的是,第三个缓存并不是总是存在的,只要当需要的时候才会创建;而且也无法完全解决Jank
现象,但是能缓解。
60Hz 和 16 ms
从上面解释帧速率时提到,虽然人眼感知生理的极限 85fps
,但达到 60fps
时动画就已经有很好的体验,不会出现卡顿和迟滞现象;而最为关键的是 60Hz
是美国交流电的频率,如果屏幕刷新频率能够匹配交流电的频率就可以有效的预防屏幕中出现滚动条;所以:
60Hz
的屏幕刷新率或者60fps
的帧率,是人眼能够感知到比较流畅的数值1000ms/60=16ms
,16ms
是指GPU/CPU
在绘制图形时,必须在这个刷新频率内绘制完成,否则会出现丢帧现象
BufferQueue
实现了整个生产者消费者模型。 BufferQueues
是 Android
图形组件之间的粘合剂。它们是一对队列,可以调解缓冲区从生产者到消费者的固定周期。一旦生产者移交其缓冲区, SurfaceFlinger
便会负责将所有内容合成到显示部分。BufferQueue
永远不会复制缓冲区内容(移动如此多的数据是非常低效的操作);相反缓冲区始终通过句柄进行传递。
BufferQueue
包含将图像流生产者与图像流消费者结合在一起的逻辑。图像生产者的一些示例包括由相机 HAL
或 OpenGL ES
游戏生成的相机预览。图像消费者的一些示例包括 SurfaceFlinger
或显示 OpenGL ES
流的另一个应用,如显示相机取景器的相机应用。BufferQueue
是将缓冲区池与队列相结合的数据结构,它使用 Binder IPC
在进程之间传递缓冲区。生产者接口,或者您传递给想要生成图形缓冲区的某个人的内容,即是 IGraphicBufferProducer
( SurfaceTexture
的一部分)。 BufferQueue
通常用于渲染到 Surface
,并且与 GL
消费者及其他任务一起消耗内容。 BufferQueue
可以在三种不同的模式下运行:
- 类同步模式
默认情况下,BufferQueue
在类同步模式下运行,在该模式下,从生产者进入的每个缓冲区都在消费者那退出。在此模式下不会舍弃任何缓冲区。如果生产者速度太快,创建缓冲区的速度比消耗缓冲区的速度更快,它将阻塞并等待可用的缓冲区。 - 非阻塞模式
BufferQueue
还可以在非阻塞模式下运行,在此类情况下,它会生成错误,而不是等待缓冲区。在此模式下也不会舍弃缓冲区。这有助于避免可能不了解图形框架的复杂依赖项的应用软件出现潜在死锁现象。 - 舍弃模式
BufferQueue
可以配置为丢弃旧缓冲区,而不是生成错误或进行等待。例如,如果对纹理视图执行GL
渲染并尽快绘制,则必须丢弃缓冲区。
为了执行这项工作的大部分环节, SurfaceFlinger
就像另一个 OpenGL ES
客户端一样工作。例如当 SurfaceFlinger
正在积极地将一个缓冲区或两个缓冲区合成到第三个缓冲区中时,它使用的是 OpenGL ES
。
数据流
Android
图形管道数据流如下图所示:
左侧的对象是生成图形缓冲区的渲染器,如主屏幕、状态栏和系统界面。 SurfaceFlinger
是合成器,而硬件混合渲染器是制作器。
组件小结
- 低级别组件
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
窗口Surface
。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
元素重绘。
高级别组件可以直接在 APP
中使用。
Buffer/Window
体系
代码速查表
1 | system/core/libcutils/include/cutils/native_handle.h |
native_handle/buffer_handle_t
先看 native_handle
这个结构体的定义:
1 | // native_handle.h |
native_handle
这个结构体,描述了一个数据结构,其中最关键的是 data[0]
,它是一个长度为 0 的数组,即 native_handle
是一个柔性数组。在标准的 C/C++
中,长度为 0 的数组是不被允许的,编译时会产生错误!长度为 0 的数组是 C/C++
的扩展,需要当前编译器支持这个扩展。从头文件注释中也可以看出,当使用的是 clang
编译器时,才会定义 data[0]
并且忽略数组为 0 的警告。
从 C/C++
中柔性数组的用途来看, native_handle
表示的是一个不定长数据结构,实际意义指向连续分配的内存空间(除了 native_handle
之外)代表的数据结构(通常是 private_handle_t
)。这里这么做,是因为显示系统和每家实现平台相关度很高, native_handle
定义一个通用的数据结构,至于显示系统如何显示,每家自己去实现对应的 private_handle_t
。
native_handle
结构体中的注释写的很清楚, numFds
表示被指向数据结构包含几个文件描述符; numInts
表示被指向数据结构长度是多少个整型;有了这两个信息后,内存分配就很容易了,参考 native_handle_create
的源码实现:
1 | // native_handle.c |
小结: native_handle, native_handle_t
表示一个不定长数据结构,而 buffer_handle_t
表示指向 native_handle
的指针。
private_handle_t
private_handle_t
描述的是一块缓存,因为和实现平台高度相关,我这里选取高通平台,先看头文件定义:
1 | // gralloc_priv.h |
private_handle_t
描述了缓存区使用的文件描述符 fd, fd_metadata
、大小、偏移量、基地址、长宽、格式、没有对齐的长宽等等,而 sNumFds
对应 nativeHandle.numFds
; sNumInts
对应 nativeHandle.numInts
,即除了文件描述符之外,该数据结构的长度。
小结: private_handle_t
在各个模块之间传递的时候很不方便,而如果用 native_handle
的来传递,就可以消除平台的差异性。一个简单示意图描述两者的关系:
至此,我们可以简单的理解为 native_handle, native_handle_t, private_handle_t, buffer_handle_t
表示的是同一块内存。
ANativeWindowBuffer
先了解 android_native_base_t
数据结构的定义:
1 | typedef struct android_native_base_t |
android_native_base_t
中 incRef/decRef
主要功能是:为了把派生类和 Android
所有 class
的老祖宗 RefBase
联系起来所预留的函数指针。
再看 ANativeWindowBuffer
数据结构的定义:
1 | // nativebase.h |
ANativeWindowBuffer
中使用了 native_handle_t
指针,同时该结构体中也有长宽、格式、步进等基本描述信息;也就是 ANativeWindowBuffer
描述的是一块 Window
相关的缓存区。
ANativeWindow
ANativeWindow
数据结构的定义:
1 | // window.h |
从数据结构定义中可以看出, ANativeWindow
和窗口属性相关,它表示的是一个底层实现的窗口,定义的各种函数指针都是对 ANativeWindowBuffer
内存的操作;而 fenceFd
可以看成这个 buffer
的锁。
小结:不管是 ANativeWindow, ANativeWindowBuffer
它们都包含 android_native_base_t
结构体,但是都没有对 incRef, decRef
赋值;可以认为 ANativeWindow, ANativeWindowBuffer
为抽象数据结构。
ANativeObjectBase
模板
ANativeObjectBase
是一个模板类,定义如下:
1 | // ANativeObjectBase.h |
ANativeObjectBase
模板类的主要作用就是实现 incRef, decRef
引用计数,以及父类子类的类型转换。
GraphicBuffer
先看 GraphicBuffer
的头文件定义:
1 | // GraphicBuffer.h |
GraphicBuffer
使用 ANativeObjectBase
模板,即 GraphicBuffer
就是 ANativeWindowBuffer
的一种具体实现;而 ANativeWindowBuffer.common
成员的两个函数指针 incRef, decRef
指向了 GraphicBuffer
的另一个基类 RefBase
的 incStrong, decStrong
;而 ANativeWindowBuffer
可以看做是把 buffer_handle_t
包了一层,所以 GraphicBuffer
也是指向的一块缓存区。
Surface
Surface
的头文件定义:
1 | // Surface.h |
Surface
也使用了 ANativeObjectBase
模板,即 Surface
就是 ANativeWindow
的一种具体实现,同样也继承了 RefBase
实现引用计数。另外成员数据结构 BufferSlot
是对 GraphicBuffer
的包装,而 mSlots
数组表示每个 Surface
中包含 NUM_BUFFER_SLOTS
个 GraphicBuffer
缓存。
而 Surface
的构造函数中,也将 ANativeWindow
的函数指针进行了赋值:
1 | // Surface.cpp |
小结
native_handle/native_handle_t
是private_handle_t
的抽象表示方法,消除平台相关性;方便private_handle_t
所表示的缓存区可以在Android
各个层次之间传递;而buffer_handle_t
是指向他们的指针ANativeWindowBuffer
将buffer_handle_t
进行了包装;ANativeWindow, ANativeWindowBuffer
都继承于android_native_base_t
,它定义了引用计数两个函数指针;可以认为ANativeWindow, ANativeWindowBuffer
为抽象数据结构,表示窗口和其对应缓存GraphicBuffer, Surface
都使用了模版类ANativeObjectBase
,都继承了RefBase
实现incRef, decRef
引用计数;它们是具体的实现类,即实现具体的窗口缓存和窗口Surface
的成员BufferSlot mSlots[NUM_BUFFER_SLOTS];
可以看作是sp<GraphicBuffer>
类型的数组;也就是说每个Surface
中都包含有NUM_BUFFER_SLOTS
个GraphicBuffer
Surface, GraphicBuffer
是图形显示系统的高层类,后续主要围绕这两个类来介绍;一个代表窗口,一个代表窗口对应的缓存
BufferQueue
中的 Buffer
对象,我们用的都是 GraphicBuffer
。 Surface
是 Andorid
窗口的描述,是 ANativeWindow
的实现;同样 GraphicBuffer
是 Android
中图形 Buffer
的描述,是 ANativeWindowBuffer
的实现。而一个窗口可以有多个 Buffer
。
libui
库
libui.so
库主要是 GraphicBuffer
缓存相关的代码,包含缓存分配,映射当当前进程等等,而 IAllocator, IMapper
具体是在 HAL
中实现的。
代码目录结构
frameworks/native/libs/ui
目录结构:
1 | ui |
GraphicBufferAllocator/GraphicBufferMapper
它们两个都是包装类,包装了 IAllocator, IMapper
,而这两个类都是在 HAL Gralloc2
中实现的。
GraphicBufferAllocator
缓存分配,包装了IAllocator
类。GraphicBufferMapper
缓存映射到当前进程,包装了IMapper
类。
GraphicBuffer
GraphicBuffer
继承 ANativeWindowBuffer
,并持有 GraphicBufferMapper
映射对应的缓存区。
1 | class GraphicBuffer : |
Fence
机制
Fence
是一种同步机制,主要用于 GraphicBuffer
的同步。它主要被用来处理跨硬件的情况,尤其是 CPU, GPU, HWC
之间的同步,另外它还可以用于多个时间点之间的同步。 GPU
编程和纯 CPU
编程一个很大的不同是它是异步的,也就是说当我们调用 GL command
返回时这条命令并不一定完成了,只是把这个命令放在本地的 command buffer
里,而 Fence
机制就是解决这些同步问题的。Fence
顾名思义就是把先到的拦住,等后来的,两者步调一致了再往前走。抽象地说,Fence
包含了同一或不同时间轴上的多个时间点,只有当这些点同时到达时 Fence
才会被触发。 Fence
可以由硬件实现 Graphic driver
,也可以由软件实现 Android kernel
中的 sw_sync
。
Fence
的主要实现代码路径:
1 | frameworks/native/libs/ui/Fence.cpp |
总得来说, kernel driver
部分是同步的主要实现,libsync
是对 driver
接口的封装, Fence
是对 libsync
的进一步的 C++
封装。 Fence
会被作为 GraphicBuffer
的附属随着 GraphicBuffer
在生产者和消费间传输; SyncFeatures
用以查询系统支持的同步机制。
DisplayInfo
显示信息
1 | // DisplayInfo.h |
DisplayInfo
结构体包含了显示屏幕的基本信息:
- 屏幕分辨率
Resolution
屏幕的宽高是用分辨率Resolution
来描述的,也就是有多少个像素点。屏幕宽度,即屏幕横向可以显示多少个像素点;屏幕高度,即屏幕纵向可以显示多少给像素点。平常所说的720P: 1080x720
屏幕,即横向可以显示 1080 个像素点,纵向可以显示 720 个像素点。如下为常见屏幕分辨率: - 屏幕
DPI
DPI: Dots Per Inch
每英寸点数,是一个度量单位,表示屏幕每英寸上有多少个物理点。常见屏幕物理大小,是用英寸来描述屏幕对角线的长度,比如IPhone X
的大小 5.8 寸,即屏幕对角线长度为 5.8 英寸。*标准DPI
为160dpi
*,人类视网膜级通常为300dpi
。PPI: Pixel Per Inch
每英寸像素,也是度量单位,表示每英寸显示多少个像素。通常情况下DPI, PPI
设为相同,表示每个物理点显示一个像素;但是好一点的显示器,可能DPI
比PPI
大,即一个像素由多个物理点来显示。 - 密度
Density
DIP: Density Independent Pixels
设备无关像素,通常简写为DP=DIP
,请注意DPI
做好区分。DP
表示这个像素的数值是和设备无关的,那实际转换时怎么转换呢?Density
密度,实际是一个缩放因子,它表示当前设备实际DPI
和标准DPI
的比例值;比如设备实际DPI
为320dpi
,那么density=320/160=2
,即density
为 2 。有了density
之后,dp, px
可以使用公式来转换px=density*dp
。
所以我们在APP
布局设计中,所有显示设置的距离,通常使用dp
来计算,来规避不同屏幕特性。 - 屏幕刷新率
FPS
这里屏幕刷新率使用FPS
来表示,不是Hz
,表示屏幕每秒能显示多少帧数据;通常为 60 fps ,即 16 ms 刷新一次。 - 屏幕旋转方向
orientations
手机默认竖屏,0 表示竖屏, 180 表示横屏。 - 屏幕安全性
secure
这主要是用于DRM
数字版权保护时,确保显示的设备是安全的,以防止DRM
的内容被在显示的过程中被截取,只有安全的设备才能显示DRM
的内容。Android
默认所有的非虚拟显示都是安全的。 appVsyncOffset, presentationDeadline
这两个都和Vsync
有关;appVsyncOffset
是一个偏移量,在系统或硬件Vsync
的基础上做一些偏移;presentationDeadline
表示,一帧数据必现在这个时间内显示出来。
libgui
库
代码目录结构
frameworks/native/libs/gui
目录结构:
1 | gui/ |
其中 H2B, B2H
表示 Framework Buffer
和 HAL
层数据结构的相互转换;实际上代表的是同一样东西,方便各层内部使用。
IGraphicBufferProducer/IProducerListener
生产者
IGraphicBufferProducer
是生产者接口,实现了 IInterface
可以用于跨进程通信。
1 | // IGraphicBufferProducer.h |
IProducerListener
是 IGraphicBufferProducer
对应的回调接口。
1 | // IProducerListener.h |
IGraphicBufferConsumer/IConsumerListener
消费者
IGraphicBufferConsumer
是消费者接口,实现了 IInterface
可以用于跨进程通信。
1 | // IGraphicBufferConsumer.h |
IConsumerListener
是 IGraphicBufferConsumer
对应的回调接口:
1 | // IConsumerListener.h |
BufferItem
BufferItem
描述了一块缓存 GraphicBuffer
,以及位置 mSlot
,同步 mFence
等等信息。
1 | // BufferItem.h |
BufferSlot
BufferSlot
记录了当前 slot
位置的缓存 GraphicBuffer
,以及对应状态, EGL
相关信息。
1 | // BufferSlot.h |
BufferQueueDefs
中定义了一个 BufferSlot
的数组结构类型 SlotsType
。
1 | // BufferQueueDefs.h |
BufferQueueCore
BufferQueueCore
是生产者消费模型的核心,如下是几个重要的成员变量:
1 | // BufferQueueCore.h |
mQueue
是一个新建先出队列,存储了一队BufferItem
数据,即一组缓存区。mSlots
一个数组,保存了BufferSlot
数据,每个slot
位置对应一个缓存。mConsumerListener
当前生产消费模型中的,消费者回调接口。mConnectedProducerListener
当前生产消费模型中的,生产者回调接口。
BufferQueueProducer/BufferQueueConsumer
生产者/消费者实现类
BufferQueueProducer
是 IGraphicBufferConsumer
的实现类,实现了生产者对应的功能。
1 | // BufferQueueProducer.h |
BufferQueueProducer
中持有 BufferQueueCore
对象; mSlots
指向 mCore->mSlots
;同时保持了生产消费模型中,对应消费者的名称。
BufferQueueConsumer
是 IGraphicBufferConsumer
的实现类,实现了消费者对应的功能。
1 | // BufferQueueConsumer.h |
BufferQueueConsumer
中持有 BufferQueueCore
对象; mSlots
指向 mCore->mSlots
。
BufferQueue
模型
BufferQueue
类是 Android
中所有图形处理操作的核心。它的作用很简单:将生成图形数据缓冲区的一方(生产者)连接到接受数据以进行显示或进一步处理的一方(消费者)。几乎所有在系统中移动图形数据缓冲区的内容都依赖于 BufferQueue
。
基本用法很简单:生产者请求一个可用的缓冲区 dequeueBuffer
,并指定一组特性,包括宽度、高度、像素格式和用法标记;生产者填充缓冲区并将其返回到队列 queueBuffer
。随后消费者获取该缓冲区 acquireBuffer
,并使用该缓冲区的内容。当消费者操作完毕后,将该缓冲区返回到队列 releaseBuffer
。
最新的 Android 设备支持“同步框架”,这使得系统能够在与可以异步处理图形数据的硬件组件结合使用时提高工作效率。例如,生产者可以提交一系列 OpenGL ES
绘制命令,然后在渲染完成之前将输出缓冲区加入队列。该缓冲区伴有一个栅栏,当内容准备就绪时,栅栏会发出信号。当该缓冲区返回到空闲列表时,会伴有第二个栅栏,因此消费者可以在内容仍在使用期间释放该缓冲区。该方法缩短了缓冲区通过系统时的延迟时间,并提高了吞吐量。
队列的一些特性(例如可以容纳的最大缓冲区数)由生产者和消费者联合决定。但是 BufferQueue
负责根据需要分配缓冲区。除非特性发生变化,否则将会保留缓冲区;例如,如果生产者请求具有不同大小的缓冲区,则系统会释放旧的缓冲区,并根据需要分配新的缓冲区。
生产者和消费者可以存在于不同的进程中; BufferQueue
永远不会复制缓冲区内容(移动如此多的数据是非常低效的操作),缓冲区始终通过句柄进行传递。
1 | // BufferQueue.h |
BufferQueue
的头文件定义很简单:
- 定义了一个
ConsumerListener
的弱引用 - 整个类只有一个函数
createBufferQueue
,它将参数中的IGraphicBufferProducer, IGraphicBufferConsumer
消费者关联起来 - 没有构造函数,只能通过
createBufferQueue
来创建对象 BufferQueue
中并不包含队列数据结构来存储缓存,仅仅连接了生产者、消费者两者的关系
来看 createBufferQueue
的具体实现:
1 | // BufferQueue.cp |
上面代码删掉了 LOG
打印及空判断,整个代码流程非常简单:
- 新建
BufferQueueCore core
- 由
core
新建生产者IGraphicBufferProducer
- 由
core
新建消费者IGraphicBufferConsumer
也就是说 BufferQueueCore
是最终的纽带,保存了生产者消费者对应的缓存区,连接了两者的关系。
Surface
Surface
代表着窗口,它包含一个生产者 IGraphicBufferProducer
用来填充缓存,也就是窗口中用来显示在屏幕上的内容,mSlot
数组表示可以有多个缓存区。
1 | // Surface.h |
ISurfaceComposer/ISurfaceComposerClient
ISurfaceComposerClient/ISurfaceComposer
都继承了 IInterface
,它们俩分别代表 Surface
合成的客户端和服务端,具体在 SurfaceFlinger
服务进程中实现, libgui
中只对合成能力(函数)做了定义。
1 | // ISurfaceComposerClient.h |
如下是 ISurfaceComposerClient
相关的类图结构:
先看 ComposerService
,它代表着 SurfaceFlinger
服务端,头文件定义如下:
1 | // ComposerService.h |
查看 connectLocked, getComposerService
两个函数的实现:
1 | // SurfaceComposerClient.cpp |
connectLocked
连接过程就是等待 SurfaceFlinger
服务启动后并获取它; getComposerService
直接返回已经连接成功的实例 mComposerService
。
再看 SurfaceComposerClient
,可以将它理解为应用端,是 SurfaceFlinger
服务的客户端,它将建立和 SurfaceFlinger
服务的通信,头文件定义如下:
1 | // SurfaceComposerClient.h |
mClient
实际对应的是 SurfaceFlinger
进程中的 Client.cpp
,查看 SurfaceComposerClient::onFirstRef
源码:
1 | // SurfaceComposerClient.cpp |
SurfaceComposerClient
中有一个重要功能就是创建 Surface
,对应源码:
1 | // SurfaceComposerClient.cpp |
最终会调用 SurfaceFlinger
中的 mClient
来创建 Layer, IGraphicBufferProducer
;而具体的 Surface
则由 SurfaceControl
来创建。
SurfaceControl
SurfaceControl
持有创建的 Surface
的强引用,头文件定义:
1 | // SurfaceControl.h |
mHandle
指向 SurfaceFlinger
创建的 Layer
。而 SurfaceControl::createSurface
直接 new
了一个 Surface
对象。
1 | // SurfaceControl.cpp |
IDisplayEventConnection
显示连接
IDisplayEventConnection
继承了 IInterface
,客户端 APP
通过它向服务端 SurfaceFlinger
发送刷新请求。
1 | class IDisplayEventConnection : public IInterface { |
IDisplayEventConnection
的类图结构
IDisplayEventConnection
的具体实现是在 SurfaceFlinger
进程中的 EventThread::Connection
;DisplayEventReceiver
持有该实例。
小结
libgui
中一共提供了 4 组IInterface
接口:IGraphicBufferProducer/IProducerListener, IGraphicBufferConsumer/IConsumerListener, ISurfaceComposerClient/ISurfaceComposer, IDisplayEventConnection
IGraphicBufferProducer/IProducerListener
生产者模型IGraphicBufferConsumer/IConsumerListener
消费者模型ISurfaceComposerClient/ISurfaceComposer
提供创建Surface
的功能,及相关管理IDisplayEventConnection
提供了APP
客户端请求服务端SurfaceFlinger
刷新的接口
SurfaceFlinger
代码速查表
1 | surfaceflinger/ |
surfaceflinger
进程
SurfaceFlinger
是以独立进程运行的,进程名为 surfaceflinger
,对应的 rc
文件如下:
1 | // surfaceflinger.rc |
surfaceflinger
服务属于核心类 core
,当 surfaceflinger
重启时会触发 zygote
的重启。接下来看进程 main
方法对应文件为:
1 | // main_surfaceflinger.cpp |
surfaceflinger
进程的 main
函数中主要做了 4 件事:
- 通过
DisplayUtils
创建SurfaceFlinger
对象 SurfaceFlinger
对象调用init
方法,实现初始化SurfaceFlinger
向系统注册Binder
服务SurfaceFlinger
对象调用run
方法,该方法是一个do-while
无限循环
初始化流程
DisplayDevice
显示设备
1 | // DisplayDevice.h |
显示设备有三种类型:主显、外显、虚显;每添加一个显示屏,都会创建一个 DisplayDevice
。
Layer
层
Layer
是 SurfaceFlinger
进行合成的基本操作单元。Layer
在应用请求创建 Surface
的时候在 SurfaceFlinger
内部创建,因此一个 Surface
对应一个 Layer
。每个 Layer
包含常见属性:
Z order
Alpha value from 0 to 255
visibleRegion
crop region
transformation: rotate 0, 90, 180, 270: flip H, V: scale
当多个 Layer
进行合成的时候,并不是整个 Layer
的空间都会被完全显示,根据这个 Layer
最终的显示效果,一个 Layer
可以被划分成很多的 Region
, 在 SurfaceFlinger
中定义了以下几种类型:
TransparantRegion
:完全透明的区域,在它之下的区域将被显示出来OpaqueRegion
:完全不透明的区域,是否显示取决于它上面是否有遮挡或是否透明VisibleRegion
:可见区域,包括完全不透明无遮挡区域或半透明区域;即visibleRegion = Region - above OpaqueRegion.
CoveredRegion
:被遮挡区域,在它之上,有不透明或半透明区域DirtyRegion
:可见部分改变区域,包括新的被遮挡区域,和新的露出区域
头文件定义:
1 | // Layer.h |
SurfaceFlinger
接收所有 Surface
作为输入,根据 Z-Order
, 透明度,大小,位置等参数,计算出每个 Surface
在最终合成图像中的位置,然后交由 HWComposer, OpenGL
生成最终的显示 Buffer
, 然后显示到特定的显示设备上。Layer
的合成分为两种,离线合成和在线合成:
- 离线合成
先将所有图层画到一个最终层FrameBuffer
上,再将FrameBuffer
送到LCD
显示。由于合成FrameBuffer
与送LCD
显示一般是异步的(线下生成FrameBuffer
,需要时线上的LCD
去取),因此叫离线合成。 - 在线合成
不使用FrameBuffer
,在LCD
需要显示某一行的像素时,用显示控制器将所有图层与该行相关的数据取出,合成一行像素送过去。只有一个图层时,又叫Overlay
技术。
由于省去合成FrameBuffer
时读图层,写FrameBuffer
的步骤,大幅降低了内存传输量,减少了功耗,但这个需要硬件支持。
Surface
创建流程图
由客户端 SurfaceComposerClient
发起创建流程,然后由服务端 SurfaceFlinger
创建对应的 Layer
,而 Layer
在被引用时会创建生产者消费者模型的 BufferQueue
,然后再由客户端将拿到的结果传入 SurfaceControl
,最后直接实例化一个 Surface
。
创建生产者消费者模型 BufferQueue
的关键代码:
1 | // Layer.cpp |
VSYNC
垂直刷新
Android
中有 2 种 VSync
信号:屏幕产生的硬件 VSync
和由 SurfaceFlinger
将其转成的软件 Vsync
信号;软件 Vsync
后者经由 Binder
传递给 Choreographer
。Vsync
信号可将某些事件同步到显示设备的刷新周期。应用总是在 VSYNC
边界上开始绘制,而 SurfaceFlinger
总是在 VSYNC
边界上进行合成。这样可以消除卡顿,并提升图形的视觉表现。 HWComposer
对象创建过程,会注册一些回调方法;当硬件产生 VSYNC
信号时,则会回调 HWC2::ComposerCallbackBridge::onVsync
方法,然后逐级回调,下图是整个回调流程图:
- 硬件
Vsync
信号发送过来,一路执行到DispSyncThread.updateModel
方法中调用mCond.signal
,唤醒DispSyncThread
线程 DispSyncThread
线程中执行EventThread::onVSyncEvent
中调用mCondition.broadcast
唤醒EventThread
线程EventThread
线程中执行DisplayEventReceiver::sendEvents
方法,会调用BitTube::sendObjects
;在MessageQueue::setEventThread
中,我们设置了BitTube
事件的回调,当收到数据会触发MQ.cb_eventReceiver
;根据Handler
消息机制,进入SurfaceFlinger
主线程SurfaceFlinger
主线程进入到MesageQueue的handleMessage
,最终调用SurfaceFlinger::handleMessageRefresh
客户端通知 SurfaceFlinger
刷新
BufferQueueProducer::queueBuffer
函数中会调用 listener->onFrameAvailable
,而这最终会触发服务端的 Layer::onFrameAvailable
,从而通知 SurfaceFlinger
合成图像。我们先看 onFrameAvailable
接口的继承关系:
从 Surface
创建流程中贴出的 Layer::onFirstRef
代码中可以看到,在 Layer
中设置了 mSurfaceFlingerConsumer->setContentsChangedListener
监听事件,所以 BufferQueueProducer::queueBuffer
会触发 Layer::onFrameAvailable
事件,下面是完整的请求流程。
客户端在 BufferQueue
中生产完图像数据后,通知 SurfaceFlinger
刷新界面的流程图:
后续
WMS
Layer
合成流程- 结合
Camera
熟悉图形显示中Buffer
相关流程 Choreographer
及掉帧分析- 详述
SurfaceTexture, SurfaceView, GLSurfaceView
等区别 - 工具
dumpsys SurfaceFlinger
输出的LOG
分析 screencap
命令及源码分析Systrace
性能工具分析