<返回更多

聊聊 Android 的 GUI 系统

2020-01-06    
加入收藏

你长得辣么好看,我想着要更详细地了解你。今天,让我们一起来聊聊 Android 的 GUI 系统。

缘起

在2019年的 google I/O 大会上,Jetpack 团队首次为大家介绍了 Jetpack Compose,这是一种全新的 Android UI 组件库。当时演讲者为大家分享了一张图,描述了 Android 10 年里的在 UI 方面简要发展历史,在长达 10 年的发展过程中,Google 针对不同的问题做出了很多的调整,但是唯独在 UI 构建方面,最初的那一套 UI 构建体系一直沿用至今,几乎没有做任何调整。

 

聊聊 Android 的 GUI 系统

 

 

Compose 可以说是 UI 体系的一种颠覆,当然,我今天并不是来推销 Jetpack Compose 的,而是因为我突然间发现,Android 诞生了这么长时间,自己也做了辣么长时间的 UI boy,可是如果你要我立马说出 View 小姐姐是怎么在屏幕上给展示出来的,我竟无语凝噎。本想着雨露均沾,结果是万花丛中过,片叶不沾身,这怎么能忍!看着UI 小姐姐那真挚的眼神,不给它扒一扒感觉都是一种罪恶。

目标

希望通过这次梳理,能对 Android 整体的 View 框架体系大致流程上能有清晰的认识。起于 App 层,止于驱动层,并且从中挑一些重要的内容来讲述,方便理清众多对象之间的关系脉络,从而在整体架构上能有比较清晰的认知。这样,在阅读源码细节时候不至于发出哲学三连问——我是谁?我从哪儿来?要到那儿去?

那么,开搞!

我是 Activity

我是一名交际花,专注于于 UI 界面显示和处理,是应用程序中各组件里人气最高的偶像之一。我在江湖中能有如此地位,那还得多亏了 Android 爸爸对我不吝的包装。对于开发者来说,只需要简单的调用setContentView、onCreate、onStart 等方法,我就能将他们想要显示的内容展现出来。很简单是吧,因为我是整个UI体系中离开发者最近的一个窗口了,只有让开发者用起来足够爽了,他们才会喜欢上我啊,所以呢,Android 爸爸也是对我花了很多小心思呢。我呢,将一系列生命周期相关的回调用模板方法模式的一种设计模式封装,然后暴露给开发者,至于一些那些粗活累活我就汇报给 Android 爸爸去处理,毕竟作为一个 idol ,人设是万万不能倒的。比如像 setContentView 这种大部分情况下只是传递了一个 xml 的布局的家伙,又要解析 View tree,又要构建的,想想都麻烦,我就很机智的交给 framework 去处理了。

你别看我多风光的样子,但是本质上,我也只是一个 window 而已啦。

我是View

我是 app 层面向开发者比较核心的 UI 相关类,目前我在源码中的实现接近 3W 行。我呢还有一个优秀的 child ,名字叫 ViewGroup。ViewGroup 通过组合模式,而能够在自身内部存在更多的 View 或 ViewGroup,这样一来,从结构上看,我们像是俄罗斯套娃,你中有我,我中有你。其实除了 View 和 ViewGroup 这些家喻户晓的明星成员外,View 家族中还有 ViewParent 、ViewRootImpl 这些重要的幕后成员,你可千万别以为 ViewParent 就是我的爹地,它虽然叫 ViewParent 但是它就是一隔壁老王,和我一毛钱关系也没有。虽然我和 ViewParent 清清白白的,但是 ViewGroup 和 ViewRootImpl 都实现了 ViewParent 的接口方法。

Activity 的setContentView()本质是要将 DoctorView,也就是 View 树的根设置到 ViewRootImpl 中。ViewRootImpl 发起遍历(调用performTraversals()函数) 后,各个 View 元素就能得到系统的最终“分配结果”。这个“分配结果”至少会包含两个方的内容:View 对象的尺寸大小位置,再加上 View 自身的 UI 内容,如此便构成了 UI 显示的基本三要素。而这重要的三要素,它们在遍历的过程中分别对应以下三个函数:

上面这三个函数是在 ViewRootImpl 中展开的,对于开发者来说,我们面对更多的则是 View 与 ViewGroup 以及它们的子类,下面是 View 相关的一些生命周期回调:

测量该控件的大小 ,如果是ViewGroup还需测量子控件大小,measureChildren或调用子控件的measure来触发子控件元素的onMeasure方法
当View分配所有的子元素的大小和位置时,在onLayout方法被调用之前getWidth(), getHeight()是获取不 到控件的大小
view渲染内容
在onDraw之后会调用此方法,分发子元素绘制,主要是针对ViewGroup。ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而不执行draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法

我是 Window

我是在应用框架层,被 JAVA 封装的用来展示窗口的一个抽象类。我负责可视化内容的排版。Android 支持的窗口类型很多,不过我们可以统一划分为三大类,即 Application Window、System Window 和 Sub Window。另外各个种类下还细分为若干子类型,这些都是在我的上司 WindowManager 通过进程通信的方式,去与后台服务 WindowManagerService 通信,最终递交到 SurfaceFlinger 来输出和呈现。

从用户的角度来说,我就是一个界面;从 SurfaceFlinger 的角度来说,我是一个 Layer ,承载着和界面有关的数据和属性;从 WMS 来说,我是一个 windowstate ,用于管理和界面有关的状态。

窗口类型与层级

Application Window 这类窗口对应应用程序的窗口,取值在 1-99 之间

Type Description FIRST_APPLICATION_WINDOW = 1 应用程序窗口的起始值 TYPE_BASE_APPLICATION = 1 应用程序窗口的基础值 TYPE_APPLICATION = 2 普通应用程序的窗口类型 TYPE_APPLICATION_STARTING = 3 应用程序的启动窗口类型。它不能由应用程序本身使用,而是Android 系统为应用程序启动前设计的窗口,当真正的窗口启动后它就消失了 TYPE_DRAWN_APPLICATION = 4 用于确保应用程序窗口在显示时已经完成了绘制 LAST_APPLICATION_WINDOW = 99 应用程序窗口的最大值

Sub Window 这类窗口将附着在其他 Window 中,取值在 1000 到 1999 之间

Type Description FIRST_SUB_WINDOW = 1000 子窗口的起始值 TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW 应用程序的 panel 子窗口,在它的父窗口之上显示 TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1 用于显示多媒体内容的子窗口,位于父窗口之下 TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2 也是一种 panel 子窗口,位于父窗口以及所有 TYPE_APPLICATION_PANEL 之上 TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3 Dialog 子窗口,如 menu 类型 TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4 多媒体窗口的覆盖层,位于 TYPE_APPLICATION_MEDIA 和应用程序窗口之间,通常透明才有意义。此类型属于未开放状态 LAST_SUB_WINDOW = 1999 子窗口的最大值

System Window 对应系统程序采用的窗口类型,取值在 2000 到 2999 之间

Type Description FIRST_SYSTEM_WINDOW = 2000 系统窗口的起始值 TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW 系统状态栏窗口 TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1 系统搜索条窗口 TYPE_PHONE = FIRST_SYSTEM_WINDOW+2 通话窗口 TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3 Alert窗口,如电量不足的提示框 TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4 屏保窗口 TYPE_TOAST = FIRST_SYSTEM_WINDOW+5 短暂的提示框窗口 TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6 系统覆盖窗口,这种类型的窗口不能接收 input 事件 TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7 电话优先窗口 TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8 RecentsAppDialog 就是这种类型的窗口 TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9 屏保时显示的对话框 TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10 系统错误窗口 TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11 输入法窗口 TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12 显示在输入法之上的对话框窗口 TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13 壁纸窗口 TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14 滑动状态栏出现的窗口 YPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19 导航栏窗口 TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20 系统音量条 TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21 开机启动的进度条窗口 TYPE_INPUT_CONSUMER = FIRST_SYSTEM_WINDOW+22 导航栏隐藏时用于消耗事件的伪窗口 LAST_SYSTEM_WINDOW = 2999 系统窗口结束

当某个进程向 WMS 申请一个窗口时,它需要指定所需窗口类型,然后 WMS 根据用户申请的窗口类型以及当前系统中已有窗口的情况来给它分配一个最终的层级值,数值越大的窗口,优先级越高,在屏幕上显示时候就越靠近用户。

窗口属性

除了窗口类型外,开发者还可以设置不同的属性来调整窗口的表现,这些属性统一放置在 WindowManager.LayoutParams 中。其中主要包括以下几个重要的变量:

Flags Description SYSTEM_UI_FLAG_VISIBLE = 0 请求显示系统UI,默认状态 SYSTEM_UI_FLAG_LOW_PROFILE = 0x00000001 低能模式,状态栏上的一些图标会被隐藏,游戏、阅读、视频播放等沉浸式应用会需要 SYSTEM_UI_FLAG_HIDE_NAVIGATION = 0x00000002 请求隐藏底部导航栏 SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004 请求全屏显示,状态栏会被隐藏,底部导航栏不会被隐藏,效果和WindowManager.LayoutParams.FLAG_FULLSCREEN相同 SYSTEM_UI_FLAG_IMMERSIVE = 0x00000800 这个flag只有当设置了SYSTEM_UI_FLAG_HIDE_NAVIGATION才起作用。如果没有设置这个flag,任意的View相互动作都退出SYSTEM_UI_FLAG_HIDE_NAVIGATION模式。如果设置就不会退出 SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000 这个flag只有当设置了SYSTEM_UI_FLAG_FULLSCREEN|SYSTEM_UI_FLAG_HIDE_NAVIGATION时才起作用。如果没有设置这个flag,任意的View相互动作都会退出SYSTEM_UI_FLAG_FULLSCREEN|SYSTEM_UI_FLAG_HIDE_NAVIGATION模式,如果设置就不受影响 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000 状态栏浅色背景模式,文字为黑色,Android 6.0以前(api < 23)不支持 SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100 请求系统UI布局稳定状态 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400 让View全屏显示,Layout会被拉伸到StatusBar下面,不包含NavigationBar SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200 让View全屏显示,Layout会被拉伸到NavigationBar下面

上面这些属性除了 systemUiVisibility 相关的是定义在 View 中的,其他的都是定义在 WindowManager 中的

我是WindowManager

我是一个继承于 ViewManager 的接口,WindowManagerImpl 是我的具体实现类。ViewManager 中定义了与 View 交互的接口函数 addView()、updateViewLayout()、removeView() ,应用程序通过(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)获取到 WindowManager 实例后,就可以通过addView()将 View 添加到 WMS 中去。

但是我只是一个接口啊,就连我的实现者 WindowManagerImpl 也没有任何持有 WSM 的影子啊。那 View 是如何添加到 WMS 中去的呢?既然我们两个没法暗通款曲,那就索性寻个媒婆来明媒正娶。这点,ViewRootImpl 是极其专业的,于是我便找了它。

我是ViewRootImpl

我是一个中介,负责管理整颗 View 树的同时,也担负着与 WMS 进行 IPC 通讯的重任。具体而言,我会通过 IWindowSession 建立双方的桥梁。

从实现上来说,在构造函数中,我会通过 WindowManagerGlobal.getWindowSession()来打开一个IWindowSession 对象来与 WMS 的可用连接。IWindowSession 是一个 IBinder 接口,它定义了一系列与 window manager 交互的交互方式,如此一来,当应用程序调用 setView 等方法时,我就可以利用它来发起一个服务请求。 IWindowSession 的服务端(Session.java)便会响应这个请求,从而调用 WMS 的 addWindow()来传递给 WMS 处理。

我是WindowManagerService

我和 AMS 等 Service 一样,是由 SystemServer 启动的系统服务的一部分。由于我是由 SystemServer 启动的,启动时机相对较晚,如果在 SystemServer 还没运行之前,我是无能为力的。比如在开机时候显示的开机动画,那时候我还没运行起来,所以这时候的显示则是由 BootAnimation 直接通过 OpenGL ES 与SurfaceFlinger 的配合来完成的。原则上我只负责“窗口”的层级和属性,之所以能够将 Window 内容显示出来,也是由于我与 SurfaceFlinger 沟通后,SufaceFlinger 才真正将窗口数据合成并最终显示在屏幕上的缘故。

从某种方面来说,我可是整个 Android UI 体系的大导演呢,因为我会根据实际情况来安排每个演员(Window)的排序站位,谁前谁后,怎么进场,如何出场等,目的当然也是为了将舞台效果和视觉美感表现得更佳,从而呈现给观众。我并不关心这里面的演员是谁,从源码角度来说,我不关心 View 树,或者说这个 window 所表达的具体类容是什么,我只要知道需要显示的界面大小,层级值等即可,而这些已经作为 WindowManager.LayoutParams 参数传递给我了。

前面说到,WMS 还需要通知 SurfaceFlinger,才能把正确的结果及时的呈现给“观众”。由于 SurfaceFlinger 绘制 UI 界面需要有“画板”—— BufferQueue 的支持,BufferQueue 在 SurfaceFlinger 中对应的是 Layer,在 Java 层对应的则是 Surface( Surface 持有 BufferQueue 的实现接口—— IGraphicBufferProducer ),因此,无论是系统窗口还是应用窗口,都必须向 SurfaceFlinger 申请相应的 Layer,进而得到图形缓冲区的使用权。

WMS、AMS 与 Activity 间的联系

Activity 运行在应用程序进程中,而 AMS 与WMS 则运行在系统相关进程中,它们之间的通信需要 Binder 的支持。应用程序访问 WMS 的服务首先要通过 ServiceManager,因为 WMS 是实名 Binder Server;WMS 还针对每个 Activity 提供了一种匿名的实现,即 IWindowSession。

当一个新的 Activity 被启动时候(startActivity),它首先需要在 AMS 中注册——此时 AMS 会生成一个 ActivityRecord 来记录这个 Activity ;另外,Activity 还承载着 UI 显示的功能,所以 WMS 也会对它进行记录——以 WindowState 来表示。WMS 除了利用 WindowState 来保存一个窗口相关的信息外,还使用 AppWindowToken 来对应 AMS 中的一个 ActivityRecord,从而将三者形成非常紧密的联系。

 

我是Surface

Surface 对应了一块屏幕缓冲区,每个 window 对应一个Surface,任何 View 都是画在 Surface 上的,传统的 view 共享一块屏幕缓冲区。

我有一个庞大的家族体系,站在台前的 Android 为我们封装的处于 java 层面的 Surface,我们家族的幕后长老们同时也在 native 层默默贡献者他们的力量。在 Surface.java中 Android 是这样定义我的 Handle onto a raw buffer that is being managed by the screen compositor. 由此可以看出,首先我是一个 raw buffer(屏幕缓冲区)的句柄,可以通过我来管理一个 raw buffer ;其次,我本身又被一个叫 screen compositor 的家伙在管理。同时,我内部持有 IGraphicBufferProducer,而这个 IGraphicBufferProducer 则是 BufferQueue 的实现接口,如此我便又和 BufferQueue 搞上了。

前面说到,WMS 想要将内容展示出来,需要我的支持,具体的,以 addView 来说,我是在 ViewRoot 进行 performTraversals 时,向 WMS 申请一个 Surface 时诞生的。WMS 在创建 Surface 时,会生成一个 SurfaceSession ,然后将这个 SurfaceSession 作为参数来构造 Surface。这个 SurfaceSession 就是 screen compositor 的一个会话链接。同时,在 java 层面上的 Surface 和 SurfaceSession 构造的时候,都会调用具体的 init 方法,唤醒我们在 native 层的长老们,他们主要聚集 framework/native/libs/gui 这个”山洞“中。

下面罗列的是其中涉及到的一些比较重要的成员和职责:

有了 Surface ,便可以得到一块屏幕缓冲区,但是这时我们的视图还是不能呈现在观众眼前的。于是便要将 Surface 添加到 BufferQueue 中,从而让 SufaceFlinger 来消费。

我是BufferQueue

我是一名勤勤恳恳的老师,我对每个应用程序都进行“一对一在线辅导”,指导着 UI 程序的 “申请画板”、“作画流程”等一系列的繁琐细节。我与各应用程序是通过 IGraphicBufferProducer 建立关系的。

BufferQueue 是 Android 显示系统的核心,它的设计哲学是生产者-消费者模型,只要往 BufferQueue 中填充数据,则认为是生产者,只要从 BufferQueue中获取数据,则认为是消费者。有时候同一个类,在不同的场景下既可能是生产者也有可能是消费者。如 SurfaceFlinger,在合成并显示 UI 内容时,UI 元素作为生产者生产内容,SurfaceFlinger 作为消费者消费这些内容。而在截屏时,SurfaceFlinger 又作为生产者将当前合成显示的UI内容填充到另一个 BufferQueue,截屏应用此时作为消费者从 BufferQueue 中获取数据并生产截图。

站在应用程序的角度来说,应用程序可以调用 createSurface 来建立多个 Layer,每一个 Layer 都对应一个 BufferQueue,换句话说,应用程序与 BufferQueue 也是一对多的关系。为应用程序申请的 Layer,一方面需要告知 SurfaceFlinger,另一方面也要记录到各 Client 内部中。另外,Layer 也没有直接持有 BufferQueue 对象,而是通过 Layer 内部的 mSurfaceFlingerConsumer 来管理的。

我是SufaceFlinger

我是由 init 进程所启动的守护进程,运行在Android系统的 System 进程中,负责管理Android系统的帧缓冲区(Frame Buffer),需要显示 UI 界面的应用程序需要通过 Binder 服务来与我通信。每个有 UI 界面的程序都在我这里有相对应的 Client 实例。应用程序与 Client 间的 Binder 接口是 ISurfaceComposerClient。Client 也只是我分配给应用程序的一个”代表“ ,真正的图行(Buffer)需要另外申请,即调用 Client 提供的 ISurfaceComposerClient::createSurface()来实现。同时,在 SufaceFlinger 进程中将会有一个 Layer 被创建,代表了一个画面。ISurface 就是控制这一面的handle,它将保持在应用程序端的 SufaceControl 中。

事实上,我是一个耿直boy,你看我的名字就知道,我的职责是 Flinger,即把系统中所以应用程序最终的“绘图结果”进行“混合”,然后统一显示到物理屏幕上。所以我不会太关注各个应用程序的“绘画过程”,于是我又派出了一个“代表”——BufferQueue 替我去完成这一光荣的使命。

现在万事俱备,只欠东风,我就可以铆足干劲哗啦啦的绘制了,观众也就能看到美轮美奂的"节目"了。那东风从哪来?又要到哪去?这时候就轮到我们勤勤恳恳的快递员选手——VSync 大展身手了。

我是VSync

谷歌在4.1版本引入了一个重大的改进——Project Butter,也即是黄油计划。Project Butter 对 Android Display 系统进行了重构,引入了三个核心元素,即 VSYNC、Triple Buffer 和 Choreographer。

安卓系统中有 2 种 VSync 信号:屏幕产生的硬件 VSync 和由 SurfaceFlinger 将其转成的软件 Vsync 信号。采用 Vsync 进行显示同步,一旦 Vsync 信号出现,CPU 便立即开始执行 Buffer 的准备工作。目前 Android 是采用 Multiple Buffer 的技术来处理的。

没有引入vsync的情况

 

聊聊 Android 的 GUI 系统

 

上图是没有引入VSync 机制的处理流程。可以看出,一个很明显的问题是,只要一次cpu/gpu 处理出现异常就可能导致后面的一系列的处理出现异常

 

引入VSync 机制

 

聊聊 Android 的 GUI 系统

 

上图是引入 VSync 机制的后的处理流程。在 FPS < 手机屏幕刷新率的情况下,一切运行完美

 

Double Buffering 异常情况

 

聊聊 Android 的 GUI 系统

 

 

上图是在 VSync 机制下,Double Buffering 时 FPS > 手机屏幕刷新率的情况。只要出现一次 Jank 就会影响下一次的 VSync (cpu 不能工作)

Triple Buffering 异常情况

 

聊聊 Android 的 GUI 系统

 

 

上图是在 VSync 机制下,Triple Buffering 时FPS > 手机屏幕刷新率的情况。当第一次 VSync 发生后,CPU 不用再等待了,除了第一次的 Jank 无法规避,第二次、第三次 VSync 到来时都能有效采用到 buffer,从而有效降低了系统显示错误。

VSync 最终会被 EventThread::threadLoop()分发给各监听者,如 SurfaceFlinger 进程中就是 MessageQueue 。VSync 被 SurfaceFlinger 监听到后,SurfaceFlinger 首先需要遍历 当前 Layer (这里的 Layer 对应的则是 BufferQueue) ,确定是否需要重绘。对应 Z-order 等与编排相关的 SurfaceFlinger可以自己确定,但是对于各个 Buffer 内容的变动,还是需要更加专业的 BufferQueue 来处理了。BufferQueue 处理完成,并且将结果返回给 SurfaceFlinger 后,再由 SurfaceFlinger 进行“加工混合”,交由 OpenGL ES 显示出来 。

我是Choreographer

字面翻译过来,我是编舞者的意思。具体来讲,我主要是配合 Vsync(因为我可以监听底层Vsync信号) ,给上层 App 的渲染提供一个稳定的 Message 处理的时机。

ViewRootImpl 启动时会初始化 Choreographer 的实例。

当 Vsync 信号由 SurfaceFlinger 中创建 HWC 触发,唤醒 DispSyncThread 线程,再到 EventThread 线程,然后再通过 BitTube(一种进程间通信的一种机制) 直接传递到目标进程所对应的目标线程,执行 handleEvent方法 ,然后通过 C++ 层的 dispatchVsync 进入到 java 层的 dispatchVsync 回调,触发FrameDisplayEventReceiver.run() 如此 Choreographer 便接收到了消息,doFrame()执行,UI 绘制开始。

参考文献


作者:joychic
链接:https://juejin.im/post/5e0ca9ccf265da5d4170e844
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>