GUI系统之SurfaceFlinger(7)应用程序的典型绘图流程

2024-04-22 17:32

本文主要是介绍GUI系统之SurfaceFlinger(7)应用程序的典型绘图流程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章都是通过阅读源码分析出来的,还在不断完善与改进中,其中难免有些地方理解得不对,欢迎大家批评指正。
转载请注明:From LXS. http://blog.csdn.net/uiop78uiop78/

GUI系统之SurfaceFlinger章节目录:
blog.csdn.net/uiop78uiop78/article/details/8954508




1.1.1 应用程序的典型绘图流程

我们知道,BufferQueue有最多达32个BufferSlot,这样设计的目的是什么?一个可能的原因就是提高图形渲染速度。因为假如只有两个buffer,可以想象一下,当应用程序这个生产者的产出效率大于消费者的处理速度时,很快它就会dequeue完所有缓冲区而处于等待状态,从而导致不必要的麻烦。当然,实际上32只是最大的容量,具体值是可以设置的,大家可以结合后面的ProjectButter小节来理解一下。

前面小节我们已经学习了BufferQueue的内部原理,那么应用程序又是如何与之配合的呢?

解决这个疑惑的关键就是了解应用程序是如何执行绘图流程的,这也是本节我们叙述的重点。不过大家应该有个心里准备,应用程序并不会直接使用BufferQueue。和Android系统中很多其它地方一样,“层层包裹”在这里同样是存在的,因而我们要尽量抓住其中的重点,并辅以一定的手段,才能更好更快地从诸多错综复杂的类关系中找出问题的答案。

出于以上原因的考虑,我们选取系统的开机动画这一应用程序,来分析整个图形绘制的流程。值得一提的是,这个开机动画的实现符合前面提到的两个改进的图形系统中的第一个,即应用程序与SurfaceFlinger都是使用OpenGL ES来完成UI显示,不过因为它是一个C++程序,所以不需要上层GLSurfaceView的支持。

当一个Android设备上电后,正常情况下它会先后显示最多4个不一样的开机画面,分别是:

l  boot-loader

这显然是第一个出现的画面。因为boot-loader只是负责系统后续模块的加载与启动,而且要求文件体积很小,所以一般我们只让它显示一张静态的图片

l  kernel

在进入内核后,同样会在物理屏幕上有所显示。和boot-loader一样,默认情况下它也只是一张静态图片

l  android(2个)

Android是系统启动的最后一个阶段,也是最耗时间的一个。它的开机画面既可以是静态文字描述、静态图片,也可以是动态画面。通常第一个是文字或者静态图片(假如指定路径下的图片不存在的话,就显示文字。关于这方面的资料很多,大家可以自行查阅,我们这里不作过多叙述),另外一个则是动画,如下图所示:


图 11‑14 原生态Android系统中的开机动画

 

这个开机动画的实现类是BootAnimation,它的内部就是借助SurfaceFlinger来完成的。另外,因为它并不是传统意义上的Java层应用程序,这使得我们可以抛离很多上层的牵绊,以最直观的方式来审视BufferQueue的使用,是分析本节问题的最佳选择。

BootAnimation是一个C++程序,其工程源码路径是/frameworks/base/cmds/bootanimation。和很多native应用一样,它也是在init脚本中被启动的,大概来看下这一过程。

service bootanim /system/bin/bootanimation

    class main

    user graphics

    group graphics

    disabled

    oneshot

以上内容是从init.rc脚本中摘录出来的,完整地描述了bootanimation这个程序的启动属性。如果大家对其中的内容不清楚的话,可以参见本书的系统启动章节。

当bootanimation被启动后,它首先会进入main函数,即main@Bootanimation_main.cpp,生成一个BootAnimation对象,并开启线程池(因为它需要与SurfaceFlinger等系统服务进行跨进程的通信)。在BootAnimation的构造函数中,同时生成一个SurfaceComposerClient:

BootAnimation::BootAnimation() : Thread(false)

{

    mSession = newSurfaceComposerClient();

}

SurfaceComposerClient是每个UI应用程序与SurfaceFlinger间的独立纽带,后续很多操作都是通过它来完成的。不过这个类只是一个封装,真正起作用的还是其内部的ISurfaceComposerClient。更多的分析我们将放在后续小节中,这里只要先知道它的功能就可以了。值得一提的是,前面小节中我们讲到了ISurfaceTexture,这里又有一个ISurfaceComposerClient,两者有什么区别呢?

简单来说,ISurfaceTexture是应用程序与BufferQueue的传输通道,而ISurfaceComposerClient则是它与SurfaceFlinger间的桥梁。这样子的设计是合理的,体现了模块化的思想——SurfaceFlinger的职责是“Flinger”,即把系统中所有应用程序的最终的“绘图结果”进行“混合”,然后统一显示到物理屏幕上。它不应该,也没有办法分出太多的精力去一一关注各个应用程序的“绘画过程”。这个光荣的任务自然而然地落在了BufferQueue的肩膀上,它是每个应用程序“一对一”的辅导老师,指导着UI程序的“画板申请”、“作画流程”等一系列细节。下面的图描述了这三者的关系:

 


图 11‑15 应用程序、BufferQueue及SurfaceFlinger间的关系

 

所以BootAnimation在其构造函数中就建立了与SurfaceFlinger的联系通道。那么它在什么时候会再去建立与BufferQueue的连接呢?因为BootAnimation继承自RefBase,当main函数中通过sp指针引用它时,会触发如下函数:

void BootAnimation::onFirstRef() {

    status_t err =mSession->linkToComposerDeath(this);//监听死亡事件

    if (err == NO_ERROR) {

        run("BootAnimation", PRIORITY_DISPLAY);//开启线程

    }

}

当一个client与远程server端的建立了binder联系后,它就可以使用这个server的服务了,但前提是服务端正常运行——换句话说,假如出现了server异常的情况,client又如何知道呢?这就是linkToComposerDeath要解决的问题,它的第一个参数指明了接收binder server死亡事件的人,在这个例子中就是BootAnimation自身。这是因为它继承了IBinder::DeathRecipient,并实现了其中的binderDied接口。

如果上一步没有出错的话(err== NO_ERROR),接下来就要启动一个新线程来承载业务了。为什么需要独立创建一个新的线程呢?前面main函数中大家应该发现了BootAnimation启动了binder线程池,可以想象在只有一个线程的情况下,它是不可能既监听binder请求,又去做开机动画的绘制的。所以当一个新的线程被run起来后,又触发了下列函数的调用:

status_t BootAnimation::readyToRun() {…  

   /*第一部分,向server端获得buffer空间,从而得到EGL需要的本地窗口*/

    sp<SurfaceControl>control = session()->createSurface(0, dinfo.w,dinfo.h, PIXEL_FORMAT_RGB_565);

   SurfaceComposerClient::openGlobalTransaction();

    control->setLayer(0x40000000);

   SurfaceComposerClient::closeGlobalTransaction();

sp<Surface> s = control->getSurface();

 

    /*以下为第二部分,即EGL的使用流程*/

    const EGLint attribs[] = {…//属性值较多,节约篇幅,我们省略具体内容};

    EGLint w, h, dummy;

    EGLint numConfigs;//总共有多少个config

    EGLConfig config;

    EGLSurface surface;

    EGLContext context;

    EGLDisplay display =eglGetDisplay(EGL_DEFAULT_DISPLAY);//第一步,得到默认的物理屏幕

    eglInitialize(display, 0,0);//第二步,初始化

    eglChooseConfig(display,attribs, &config, 1, &numConfigs);//第三步,选取最佳的config

    surface =eglCreateWindowSurface(display, config, s.get(),NULL);//第四步,通过本地窗口创建Surface

    context =eglCreateContext(display, config, NULL, NULL);//第五步,创建context环境

    …

    if(eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)//第六步,设置当前环境

        return NO_INIT;

    …

    return NO_ERROR;

}

从这个函数中不但可以看出应用程序是如何使用BufferQueue的,而且还有另外一个重要的学习点,即Opengl ES与EGL的使用流程。在本书应用篇章中我们已经给出了EGL的使用实例,这里则可以做为第二个例子。

函数首先通过session()->createSurface()来获取一个SurfaceControl。其中session()得到的是mSession变量,也就是前面构造函数中生成的SurfaceComposerClient对象,所以createSurface()最终就是由SurfaceFlinger来实现的。只不过SurfaceFlinger中返回的其实是一个ISurface对象,本地端的SurfaceComposerClient又包装了一层,变成了SurfaceControl,言下之意就是对ISurface进行管理。大家肯定会有疑惑,ISurface从哪冒出来的,做什么的?要回答这点不难,只要看下SurfaceFlinger中最终是传了个什么对象过来就行:

sp<ISurface> SurfaceFlinger::createSurface(…)

{

                sp<LayerBaseClient>layer;

                sp<ISurface>surfaceHandle;

    //中间省略layer的生成过程

                surfaceHandle =layer->getSurface();

                returnsurfaceHandle;…

我们省略了中间一大段过程,只保留与问题相关的部分,更详细的分析可以参见后面的SurfaceFlinger小节。从中可以清楚看到,ISurface是通过layer->getSurface()得到的。Layer类在SurfaceFlinger中表示“层”的概念,而ISurface则是客户端与这个“层”进行沟通的通道。通俗地讲“层”就代表了一个“画面”,最终的显示结果就是通过对系统中同时存在的所有“画面”进行处理得到的。打个比方来说,就好像是一排人各举着一张绘画作品,那么观察者从最前面往后看时,他首先可以看到的就是第一张画。而假如第一张画恰好比第二张小,又或者第一张是透明/半透明的(这并非不可能,比如作者是在玻璃上创作的),那么他才能看到第二张画,以此类推。。。

这个比喻告诉我们,layer是有层级的,越靠近用户的那个“层”就越有优势。

明白了这个道理,函数接下来调用setLayer就不难理解了。不过参数中传入了一个数值0x40000000,这又是什么意思?其实这个值就是layer的层级,数字越大就越靠近用户,在显示系统中我们通常称为z-order。以后在Window Manager Service章节的分析中还会看到对setLayer的调用,因为此时系统中还只有开机画面一个应用程序,所以我们还不需要担心z-order的问题。

设置完层级后,我们接着control->getSurface()来得到一个Surface对象。相关的类越来越多了,而且由于命名上的不恰当,进一步加剧了大家理解上的困难。其实可以来猜测下这个类是做什么的?根据如下:

l  第二部分中的eglCreateWindowSurface使用了Surface,证明它必然是一个本地窗口

l  前几个小节我们介绍的两种本地窗口,一个是FramebufferNativeWindow,另一个是SurfaceTextureClient,那么看来Surface必然要与其中一个有关联。很显然的,运行于应用程序端的本地窗口必然是SurfaceTextureClient,我们可以从Surface的继承关系中得到验证:

class Surface : public SurfaceTextureClient

的确是太乱了,我们有必要先来整理下目前已经出现的容易混淆的相关类的关系。

ISurfaceComposerClient: 应用程序与SurfaceFlinger间的通道,在应用进程中则被封装在SurfaceComposerClient这个类中。这是一个匿名binder server,由应用程序(具体位置在SurfaceComposerClient::onFirstRef中)调用SurfaceFlinger这个实名binder的createConnection方法来获取到,服务端的实现是SurfaceFlinger::Client。

ISurface:由应用程序调用ISurfaceComposerClient::createSurface()得到,同时在SurfaceFlinger这一进程中将会有一个Layer被创建,代表了一个“画面”。ISurface就是控制这一画面的handle。

Surface:从逻辑关系上看,它是上述ISurface的使用者。从继承关系上看,它是一个SurfaceTextureClient,也就是本地窗口。SurfaceTextureClient内部持有ISurfaceTexture,即BufferQueue的实现接口。换个角度来思考,当EGL想通过Surface这个native window完成某些功能时,后者实际上又利用ISurface和ISurfaceTexture来取得远程服务端的对应服务,以完成EGL的请求。

回到BootAnimation::readyToRun()中来。因为本地窗口Surface已经成功创建,接下来就该EGL上场了,具体流程我们在代码中都加了注释,这里就不赘述了。

当EGL准备好环境后,意味着程序可以正常使用opengl ES提供的各种API函数进行绘图了。这部分实现就集中在随后的threadLoop()以及android()/movie()中。因为不属于本小节的讨论范围,有兴趣的读者可以自行参阅学习。

最后来做下小结,一个典型的应用程序使用SurfaceFlinger进行绘图的流程如下图所示:


图 11‑16 应用程序通过SurfaceFlinger进行绘图的典型流程

 

上图是从时序纵向角度总结出来的流程,我们再从横向的角度来看下,应该就更清楚了。


图 11‑17 横向角度考查Surface相关类的关系

 

可以看到,涉及到的类还是比较多的,而且多数都涉及到跨进程通信。希望大家能熟悉这两张图,在后几个小节的学习中,如果觉得混乱的时候也可以回头来看下,加深印象。


这篇关于GUI系统之SurfaceFlinger(7)应用程序的典型绘图流程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/926416

相关文章

Linux流媒体服务器部署流程

《Linux流媒体服务器部署流程》文章详细介绍了流媒体服务器的部署步骤,包括更新系统、安装依赖组件、编译安装Nginx和RTMP模块、配置Nginx和FFmpeg,以及测试流媒体服务器的搭建... 目录流媒体服务器部署部署安装1.更新系统2.安装依赖组件3.解压4.编译安装(添加RTMP和openssl模块

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

springboot启动流程过程

《springboot启动流程过程》SpringBoot简化了Spring框架的使用,通过创建`SpringApplication`对象,判断应用类型并设置初始化器和监听器,在`run`方法中,读取配... 目录springboot启动流程springboot程序启动入口1.创建SpringApplicat

CentOS系统Maven安装教程分享

《CentOS系统Maven安装教程分享》本文介绍了如何在CentOS系统中安装Maven,并提供了一个简单的实际应用案例,安装Maven需要先安装Java和设置环境变量,Maven可以自动管理项目的... 目录准备工作下载并安装Maven常见问题及解决方法实际应用案例总结Maven是一个流行的项目管理工具

通过prometheus监控Tomcat运行状态的操作流程

《通过prometheus监控Tomcat运行状态的操作流程》文章介绍了如何安装和配置Tomcat,并使用Prometheus和TomcatExporter来监控Tomcat的运行状态,文章详细讲解了... 目录Tomcat安装配置以及prometheus监控Tomcat一. 安装并配置tomcat1、安装

MySQL的cpu使用率100%的问题排查流程

《MySQL的cpu使用率100%的问题排查流程》线上mysql服务器经常性出现cpu使用率100%的告警,因此本文整理一下排查该问题的常规流程,文中通过代码示例讲解的非常详细,对大家的学习或工作有一... 目录1. 确认CPU占用来源2. 实时分析mysql活动3. 分析慢查询与执行计划4. 检查索引与表

Git提交代码详细流程及问题总结

《Git提交代码详细流程及问题总结》:本文主要介绍Git的三大分区,分别是工作区、暂存区和版本库,并详细描述了提交、推送、拉取代码和合并分支的流程,文中通过代码介绍的非常详解,需要的朋友可以参考下... 目录1.git 三大分区2.Git提交、推送、拉取代码、合并分支详细流程3.问题总结4.git push

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

PyCharm接入DeepSeek实现AI编程的操作流程

《PyCharm接入DeepSeek实现AI编程的操作流程》DeepSeek是一家专注于人工智能技术研发的公司,致力于开发高性能、低成本的AI模型,接下来,我们把DeepSeek接入到PyCharm中... 目录引言效果演示创建API key在PyCharm中下载Continue插件配置Continue引言