当前位置: 首页 > 科技观察

深入理解Android图形系统

时间:2023-03-18 01:50:59 科技观察

一、图形系统简介图形系统是计算机中最重要的子系统之一。我们平时使用的电脑、手机都是图形界面。对于普通人来说,没有图形界面的电脑几乎没有用。今天我们将讨论图形系统背后的原理。1.1图形系统的诞生早期的计算机没有图形界面,都是命令行界面。每个人都坐在终端前输入命令,执行命令,等待命令完成,等等。这样的电脑更适合科研人员和科技人使用,但不可能普及到千家万户。后来,施乐帕洛阿尔托研究中心(XeroxPARC)率先研究了具有图形界面的计算机,并提出了WIMP的概念。WIMP是Window(窗口)、Icon(图标)、Menu(菜单)、Pointer(指针/鼠标)。我们当前的计算机仍处于WIMP模式。遗憾的是,施乐并没有做出带有图形界面的电脑,而是被乔布斯和比尔盖茨发扬光大了。当乔布斯参观帕克研究中心时,他被他们展示的图形界面惊呆了。回来后,他马上在公司里搞了一个图形界面的操作系统。比尔盖茨发现苹果的图形界面确实不错,他也开始做自己的图形界面,于是就有了Windows系统。后来苹果和微软因为图形界面的问题打了官司。1.2图形系统的总体结构与命令行模式相比,图形模式的编程方式和软件结构都发生了很大的变化。在命令行模式下,程序员只需考虑程序本身的进程,然后通过标准输入输出与终端打交道即可。但是当谈到图形模式时,一切都变了。程序员首先要考虑如何绘制程序的界面,然后通过消息循环处理程序的点击等各种事件。不仅程序员的编程方式发生了变化,操作系统的实现方式也发生了很大的变化。在命令行模式下,操作系统只需要提供一个shell,shell不断的读取和执行命令。但是在图形方式下,操作系统首先要提供一个桌面作为用户使用计算机的起点,同时还要提供一个文件管理器,方便用户查看和管理文件。对于程序员来说,操作系统还提供了图形编程接口,提供了渲染库,还负责合成和显示所有窗口。于是在操作系统中诞生了一个重要而庞大的子系统——图形系统。根据前面几句的描述,我们先来看一下图形系统的简单结构。可以看出图形系统的整体结构还是比较简单的。GUI进程需要窗口系统创建和管理窗口,渲染系统帮助绘制界面,最后显示系统将画面显示在监视器上。1.3图形系统各层的作用了解了图形系统的总体结构,下面详细介绍一下各层的功能。窗口系统一般作为一个进程运行在用户空间,我们称它的进程为DisplayServer。窗口系统有两个职责:一个是窗口管理器,负责窗口的创建、缩放和销毁;另一个是合成管理器,负责将每个GUI进程绘制成位图后的窗口进行合成,然后发送给显示系统进行显示。渲染系统以so库的形式存在,加载到各个GUI进程的内存空间中。渲染系统负责执行GUI进程的绘图命令,并在窗口的显示缓冲区上生成相应的位图。渲染分为2D渲染和3D渲染。2D渲染一般由CPU执行,3D渲染一般由GPU执行。但现在2D渲染通常也由GPU完成。但是很多普通的程序并不直接使用渲染库,而是使用控件库,因为直接使用渲染库太麻烦了。比如我们要绘制一个按钮,使用渲染库API绘制很麻烦,但是如果使用控件库API,我们只需要指定position,size,和风格。显示系统以驱动的形式存在于内核中,驱动是屏幕控制器的驱动或DPU的驱动。显示系统的作用是在监视器上显示一个由所有窗口组成的位图。早期的显示驱动模型是FBDEV,它是针对屏幕控制器的。屏幕控制器没有计算能力,只能接收窗口系统合成的位图进行显示。这时候窗口系统的合成管理器就会使用渲染系统来合成各个窗口的位图,合成也可以看作是一种特殊的渲染。后来屏幕控制器逐渐发展成为DPU,具有计算能力,可以进行复合运算。这时,一种新的显示驱动模型DRM也诞生了。DRM让窗口系统不进行合成操作,而是将每个窗口的显存发送给自己,通过DPU进行合成操作,然后发送到显示器显示。2.Android图形系统Android是目前最流行的移动操作系统之一。今天我们就来详细分析一下Android图形系统。2.1框架概述在谈Android之前,我们先来看看Linux发行版的图形系统。由于Android内核也是Linux,所以它们的显示系统是一样的。Linux渲染系统使用OpenGL和最新的Vulkan,控制库使用GTK(GNOME)或Qt(KDE)。Linux的窗口系统有着悠久而复杂的历史,可以追溯到UNIX时代。这里不细说,只说现状。在Linux上,窗口系统的协议和实现是明确分开的。Linux长期使用的窗口协议称为XWindow,实现是X.org。但是由于XWindow过于陈旧,很多设计不符合现状,历史包袱依然很重。因此,有人设计了一个新的窗口协议Wayland,Wayland最流行的实现叫做Weston。现在大多数Linux发行版都开始转向Wayland/Weston。了解了Linux发行版的图形系统,我们再来看看Android的图形系统。Android的图形系统并没有明确的约定来实现这两个协议。这是因为Linux系统是一个标准的开源系统。很多事情喜欢先定一个协议,然后任何人都可以执行这个协议。Android虽然也是开源的,但是是谷歌直接实现,其他厂商使用的,所以不需要建立协议。Android图形系统在具体细节上与Linux图形系统有很大的不同。这是因为Linux图形系统是面向桌面系统的,而Android图形系统是面向移动系统的。两者的使用环境和开发环境不同,导致不同的实现细节。最大的区别之一是Android图形系统中没有典型的窗口概念。在其他的窗口系统中,一般都有一个CreateWindow接口用来创建一个窗口,返回值是一个窗口句柄,然后我们就可以使用这个窗口句柄来做其他的事情了。但是在Android中,没有这样的逻辑。窗口的概念在具体实现中是隐藏和散布的。程序员面对的是Activity、View、ViewGroup。以下小节将介绍Android图形系统的各个部分。2.2渲染系统概述在Android中,一开始是用OpenGLES做3D渲染,用skia做2D软件渲染。后来为了优化2D渲染,开发了硬件渲染hwui。hwui是OpenGLES的一个包。后来hwui调用了skia,skia封装了OpenGLES进行硬件渲染。当然,skia也保留了软件渲染部分。让我们看看下面的图片。OpenGLESsystemwrapper是系统提供的标准接口库。它的so位置是固定的,方便程序加载。其界面为标准界面,方便程序使用。但是它本身并没有任何实现逻辑,所有的实现逻辑都在GPU厂商提供的非开源库中。普通的APK并不直接使用这些渲染库,而是使用系统提供的控件库。Android提供的大部分控件都在android.view和android.widget包中。2.3窗口系统概述窗口系统有两个职责,窗口管理器和组合管理器。在Android中,两者并不在一起。窗口管理器是在system_server进程中实现的,名字叫WindowManagerService(WMS),是Java语言实现的,因为system_server是Java进程。为什么Android要在system_server中实现WindowManagerService?这是system_server中的ActivityManagerService(AMS)。两者关系比较密切,放在一起比较合适。组合管理器在一个名为SurfaceFlinger的单独进程中实现。一开始直接合成了SurfaceFlinger。后来由于硬件合成的兴起,SurfaceFlinger不再直接进行合成操作,而是将合成操作转发给底层。WindowManagerService和SurfaceFlinger使用Binder进程间通信进行交互。让我们看下图:谷歌推出了一个名为HWC(硬件合成器)的模块来处理硬件合成。最开始HWC只是一个so库,运行在SurfaceFlinger进程中。后来HWC独立成了一个单独的进程。在HWC中,有很多厂商提供的非开源和半开源库。这张图片中的图片和APK没有交互。window系统与APK的交互分为两部分。一是程序在创建Activity的时候会和WMS交互创建一个窗口。Android中没有典型的窗口概念,PhoneWindow、DecorView、ViewRootImpl、Surface可以组合在一起作为窗口的概念。还有一部分没有画出来,就是APK渲染和SurfaceFlinger合成的生产者消费者关系。这个逻辑将在下一章讨论。如果想深入学习AMS,推荐阅读老罗Android之旅中的WMS文章:https://blog.csdn.net/Luoshengyang/article/details/8462738,以及袁慧慧写的WMS解析:http://githuan.com/2017/01/08/windowmanger/2.4显示系统概述显示系统与屏幕直接相关,属于内核中的驱动程序。对于任何一种硬件,内核一般都有一个驱动模型,所有的硬件厂商都基于这个硬件模型开发驱动。FBDEV首先被添加到从显示中抽象出来的驱动程序模型中。后来随着硬件和软件的发展,诞生了新的驱动模型DRM。现在大部分系统都在转向DRM,所以我们这里说说DRM。先画个图看看:这个结构体其实就是很多驱动的一个结构体。内核定义并实现了DRMCore,硬件厂商根据DRMCore的要求扩展结构,实现函数指针,然后调用注册函数对自己进行注册。用户空间使用的硬件会创建一个设备文件。用户空间可以打开设备文件,使用ioctl调用各种命令。ioctl命令由Core定义,具体的驱动需要实现这些命令。直接在用户空间使用ioctl命令还是比较麻烦的,所以会有一个libdrm库,将各种ioctl命令封装起来,转换成函数接口,这样使用起来更方便。3.生产者-消费者模型在说渲染和合成之前,我们先说说它们之间的关系,以及它们交互的过程。3.1概述渲染和合成是生产者和消费者的关系,那么它们是如何交互的呢?Android实现了一个生产者-消费者模型BufferQueue,生产者和消费者通过BufferQueue进行交互。BufferQueue管理GraphicBuffer,producer渲染的内容放在GraphicBuffer上,consumer合成的内容的来源来自GraphicBuffer。GraphicBuffer通过Google定义的Hidl接口Gralloc分配内存,Gralloc通过ION分配内存。ION是一种基于DMA-BUF的跨空间、跨设备的内存分配方式。为了加快产生消费的过程,BufferQueue可以采用异步方式。异步的时候,需要同步步调。为此采用的方法是Fence。Fence是一种跨空间、跨设备的同步机制。跨空间是指进程与进程之间、内核空间与用户空间之间,跨设备是指两个设备的驱动程序之间或驱动程序与进程之间。我们画一张图看看他们的整体关系。3.2BufferQueueBufferQueue是Android中渲染和合成的生产-消费关系模型的实现。我们先来看看如何使用BufferQueue。voidBufferQueue::createBufferQueue(sp*outProducer,sp*outConsumer){sp核心(newBufferQueueCore());sp生产者(newBufferQueueProducerBufferConsumer();消费者(新BufferQueueConsumer(核心));*outProducer=生产者;*outConsumer=consumer;}可以看到创建BufferQueue就是创建一个BufferQueueCore,然后以这个core为参数创建生产者的基础接口和消费者的基础接口。一般在消费者进程中创建BufferQueue,然后使用Binder将生产者接口跨进程传递给生产者进程。当然也可以反过来,或者两者都可以跨进程,或者都不可以跨进程。我们之所以选择在消费者进程中创建BufferQueue,大多数情况下是为了让消费者做好准备,然后生产者的生命周期成就可以立即被消费。一般情况下,我们不会直接使用原来的生产者或消费者接口,而是将它们一层层封装起来,封装后的接口使用起来更方便。我们来看看它的封装逻辑图。此图显示了APK和SurfaceFlinger对BufferQueue的使用。可见原来producer接口的封装一般都是Surface,但是我们在代码中经常看到SurfaceControl,这是怎么回事呢?这是为了完成控制权和提款权的分离。APK启动时,会请求WMS创建一个window,也就是Surface,WMS会请求SurfaceFlinger创建BufferQueue,并获取其原始生产者接口。WMS本身将原来的producer封装为SurfaceControl来控制Surface。然后将原生产者打包成一个Surface传给APK,让APK只有绘制权。如果APK想要设置Surface属性,它必须向WMS寻求帮助。我们画个图看看:下面我们看一下BufferQueue的内部管理逻辑。BufferQueue管理GraphicBuffer,但不直接管理GraphicBuffer,而是定义了BufferSlot结构体。BufferSlot包含一个指向GraphicBuffer的智能指针应用程序和一个指向Fence的智能指针引用,以及BufferState。BufferQueueCore包含一个包含64个元素的BufferSlots数组。由于BufferSlots都是被智能指针引用的,所以它们最初是空的,只有在使用时才会分配。BufferQueue在管理BufferSlots时并不直接操作它们,而是管理它们的下标。我们画个图看看。BufferQueue使用4个整型容器来管理BufferSlots,BufferSlots的下标在不同的容器中有不同的含义。首先,BufferQueue硬编码定义64为所有slot。创建BufferQueue后,我们可以使用接口函数来设置我们要使用多少个Buffer。未使用的下标将放置在容器mUnusedSlots中,使用过的下标将放置在容器mFreeSlots中。那么当我们使用一个Buffer的时候,不管它是被生产者使用还是被消费者使用,它的下标都会被放到容器mActiveBuffers中。当消费者使用完一个Buffer后,它会被放入容器mFreeBuffers中。mFreeBuffers和mFreeSlots的区别在于前者的BufferSlot已经关联了GraphicBuffer,而后者只是一个空槽。我们将在3.4节中讨论Buffer的状态转换。3.3内存分配与同步当我们第一次使用BufferSlot时,我们会分配GraphicBuffer,那么GraphicBuffer是如何分配内存的呢?GraphicBuffer会通过Google定义的Gralloc接口分配内存。Gralloc接口是通过两个Hidl接口IAllocator和IMapper实现的。我们画个图看看。可以看到最终分配内存的方法是ION。ION是一种跨空间、跨设备的内存分配方式。ION基于DMA-BUF。先说DMA-BUF。DMA-BUF是一种跨空间、跨设备的内存共享机制。它只是一个框架,不能分配内存。DMA-BUF既不是DMA也不是BUF,而是Sharing。DMA-BUF定义了两个角色:Exporter(导出器),负责分配内存,一个系统中只能有一个importer;Importer(导入者),也叫User,负责使用内存,可以有N个,一般有2个,一个write,既是producers,一个reads,既是consumer。画个图看看:理解了DMA-BUF,再来看看ION。ION是建立在DMA-BUF的基础上的。由于DMA-BUF,ION可以在进程之间、进程和内核之间以及设备之间共享内存。ION本身有很多堆,不同的堆用来分配不同类型的内存。ION默认使用系统堆。内核中的代码可以直接使用ION接口。为了让用户空间也能使用ION,ION创建了一个设备文件/dev/ion。用户空间可以通过各种ioctl命令来使用ION,这显然不方便,所以创建了libion??来帮助大家方便的使用ION。综上所述,如下图所示:GraphicBuffer内存分配完成后,可以用于渲染和合成。但是现在我们只能进行同步操作,而GPU渲染是异步的。为了提高性能,我们需要异步使用下的等待通知机制。为此,在内核中实现了Fence,主要用于DMA-BUF,因此也是一种跨空间、跨设备的机制。因此,Fence是一种跨设备、跨空间的等待/通知机制。它与Java中的wait/notify和C++中的wait/signalofconditionvariables具有相同的语义。不同的是Java和C++中的机制只能在进程内使用。Fence的另一大特点是它的notify信号不会丢失。这是因为Fence是一次性使用的,用完就可以扔掉。每次使用都需要重新申请,不能重复使用。因此,Fence被编号。的。Fence不仅有编号,还有上下文,可以在不同的场景下创建。同一上下文下的栅栏数具有可比性,以较小的时间在前。不同背景下的栅栏数没有可比性。我们画个图看看。3.4生产和消费过程了解了前面的知识之后,我们来看一下生产和消费的具体过程。我们先来看一下BufferSlot的状态转换。BufferSlot的状态变化与生产和消费的过程有关。我们先看图再解释。BufferSlot最初处于Free状态。当生产者准备生产时,它会先将Buffer出队。这时会获取到BufferSlot,BufferSlot的状态也会变为Dequeued。如果之前获取到的BufferSlot是一个空slot,就会分配内存。该过程在上一节中提到。如果是已经分配内存的槽,则直接使用。然后producer开始生产,将生产出来的内存全部放入GraphicBuffer中。当生产完成后,会调用queueBuffer告诉消费者我生产结束了,你可以开始消费了。queueBuffer之后,BufferSlot的状态由Dequeue变为Queued。这时GraphicBuffer会被封装成一个BufferItem结构,放入mQueue队列中。消费者得到消息后,准备消费。consumer先获取Buffer,从mQueue队列中获取一个BufferItem,对应的BufferSlot的状态转换为Acquired。然后消费者就可以开始消费了。当消费完成后,会调用releaseBuffer表示自己消费完成,并将BufferSlot返回给BufferQueue。这时BufferSlot的状态又回到了Free。了解了BufferSlot的状态变化和产生消费的基本过程后,我们再来看看VSyncwithFence下产生消费的过程。先看图:首先,渲染和合成是两个独立的线程,两者是同时进行的,双方都在收到VSync信号时开始执行。其次,渲染和合成都有一个额外的线程用于异步渲染和合成,不会阻塞主流线程。主流线程没有阻塞操作,不会卡死。两个异步线程都在等待Fence信号,可能会卡住。当一个异步线程已经卡住了,比如合成线程卡住了,会导致渲染线程卡在waitfencesignal,但是主线程可以继续运行。4.总结与回顾通过本文,我们对Android的图形系统有了一个基本的了解,对图形渲染与合成的生产者-消费者模型有了一个大概的了解。我们看图再复习一下:图形系统由渲染系统、窗口系统、显示系统三部分组成。渲染系统负责帮助GUI进程绘制界面。Surface进行合成,显示系统负责将合成后的图像发送到显示器进行显示。现在比较流行硬件合成,窗口系统通过硬件把图形合成的任务交给显示系统来完成。参考资料:https://blog.csdn.net/hexialong2009/category_9281458.htmlhttps://blog.csdn.net/hexialong2009/category_9705063.htmlhttps://blog.csdn.net/hexialong2009/category_10331964.htmlhttp://www。wowotech.net/sort/graphic_subsystem作者简介:程雷,某手机大厂系统开发工程师,读码界名誉主编,最大的爱好就是钻研Linux内核的基本原理。