Overview
早年的Android系统UI流畅性差的问题一直饱受诟病,Google为了解决这个问题开发了Project Butter项目,也就是黄油计划,期望彻底改善Android系统的流畅性。这是Android UI系统的一次非常大的改进,了解改进的内容,是我们掌握Android渲染机制的关键。
概括来说在这次改进中,Google打出了一套VSync+Choreographer+TripleBuffer的组合拳。具体说来;
●VSync:它是黄油计划的核心,VSync(Vertical Synchronization 垂直同步)是一种在PC时代就广泛使用的技术,简单说来它是利用屏幕刷新的间隙来进行帧缓冲区交换的技术。
●Choreographer:Choreographer的引入是为了配合VSync,给App端一个稳定的渲染处理时机。
●TripleBuffer
我们分别来看看。
VSync
我们知道屏幕上的画面每一帧都是静态的,不同的帧不断的进行刷新我们才能看的动态的画面,对于Android来说,界面的刷新是16.67ms刷新一次,也就是60fps(frame per second,每秒更新60次),为什么是60fps呢?
60fps是跟硬件屏幕的刷新率有关的,为了和主流屏幕的刷新率保持一致,目前主流的屏幕是60HZ(当然现在还有90HZ,120HZ),对于屏幕来说更高的刷新率就意味着更高的功耗,以及更短的TFT数据写入时间,对屏幕来说,设计难度也就更大。
那么Android的UI系统,怎么和屏幕刷新做同步呢,这就要提到VSync机制,什么是VSync机制?
简单来说,VSync机制就是把UI体系的FPS和显示器的刷新频率同步起来,避免屏幕界面出现“断裂”的现象。
我们以VSync为中心,来用一组图来解释这几个概念,这些图来自于Google I/O 2012关于Project Butter的主题演讲。
●黄色:显示器
●绿色:GPU
●蓝色:CPU
三者被等分成16.67ms的段,来观察每个周期内的渲染情况。
当不使用VSync机制时,在标号2中由于CPU没有及时处理这一帧,导致显示器只能继续显示第一帧,这种情况称为Jank。为什么CPU没有及时处理呢,可能是CPU在忙于处理其他事情,忘记了处理UI绘制。当CPU想起来要去处理UI绘制时,又过了最佳时间段,
为此就引入了Vsync,它类似于一种中断机制,通知CPU去处理UI绘制。如下所示:
由于使用了VSync机制,CPU可以早早的开始处理标号2的绘制,这样显示器也能正常的显示标号2的数据了。
这似乎非常完美,由于由VSync这种同步机制的存在CPU/GPU与显示器的FPS都保持了一致,如我们上文所说是60fps。负责在VSync信号到来时,实现这种同步工作的正是Choreographer。
但是这都建立在CPU和GPU都能在一个周期内(16.67ms)完成自己的工作,如果不能呢,如下所示:
GPU没有在一个周期内处理完B,导致Display只能继续显示A,这样AB两个Buffer(Android早期是Double Buffer机制,一个Back Buffer供GPU和CPU使用,一个Front Buffer供Display使用)都被占用了。这样就导致CPU在第二个周期内无所事事。不仅如此,由于CPU在等待A释放,导致CPU绘制被延期,有导致了下一个Jank。
这个时候如果有第三个Buffer,CPU就能去干活了。这也就是Tripple Buffer机制。如下所示:
CPU使用第三个Buffer C进行绘制,虽然A在Display里还是重复显示了一次,但是后续的绘制流程就比较流畅了。那Buffer是不是越多越好呢,答案是并不是,CPU绘制的Buffer C数据要等待第四个周期才显示,这比Double Buffer多了16.67ms的延迟,正常情况下都是两个或者三个(Chromium就是Double Buffer机制)。
什么是VSync机制?
VSync(Vertical Synchronization 垂直同步)可以同步应用渲染、屏幕合成以及屏幕刷新周期的时间,消除卡顿,提升图形的视觉表现。VSync信号由HardwareCompositor产生,它封账了硬件厂商提供的HAL层,如果HAL层能产生VSync信号,则直接使用硬件VSync信号,否则使用HardwareCompositor内部的VSyncThread模拟产生软件VSync信号(Sleep固定时间,然后唤醒)。
HardwareCompositor产生HW_VSYNC信号,经由DispSync生成SW_VSYNC信号,SW_VSYNC信号经过offset调整生成了两种信号:
●VSYNC_APP:Choreographer使用,Choreographer会配合VSync,给上层App一个稳定的渲染知己,上面提到VSync的触发周期是16.67ms,每隔16.67ms,VSync信号就会唤醒Choreographer来做App的绘制操作。
●VSYNC_SF:SurfaceFlinger使用。SurfaceFlinger会在VSync信号到来的时候,进行合成操作。
其中phase-sf和phase-app就是一个vsync offset(0-16.67ms),这个值可以通过adb shell dumpsys surfaceflnger获得。
什么是vsync offset,简单来说它是VSync信号的偏移,正常情况下VSYNC-app和VSYNC-sf信号时同步到来的,一个生产当前帧的数据,一个消费上一帧的数据,VSync Offset可以让VSYNC信号发生偏移,比如让VSYNC-sf信号提前到来,提前处理当前这一帧的数据。
有了VSync机制,Android UI系统就可以在VSync信号的驱动下有条不紊的进行渲染和刷新了。我们来看看具体的实现。
Choreographer
相关源码
●Choreographer.java
Chroreographer的引入主要是为了配合VSync信号,给上层App渲染一个稳定的Message处理时机,它在Android渲染流水线中扮演着承上启下的角色。
●向下负责接收和处理App的各种页面更新消息和回调(例如Input、Animation、Traversal等),等到VSync到来的时候统一处理,以及判断卡顿掉帧情况,记录回调耗时等。
●向下负责请求(FrameDisplayEventReceiver.scheduleVsync,当应用需要绘制UI时,会申请一次VSync中断,然后再在中断处理的onVSync函数中进行绘制)和接收(FrameDisplayEventReceiver.onVsync())VSync信号。
工作流程
1Choreographer初始化,初始化FrameHandler,绑定Looper;初始化FrameDisplayEventReceiver,它会创建一个IDisplayEventConnection的VSync监听者对象,与SurfaceFlinger监理通信用来请求和接收VSync信号。
2SurfaceFlinger的appEventThread唤醒并发送VSync信号,触发Choreographer回调FrameDisplayEventReceiver.onVsync(),进入Choreographer的主要处理函数doFrame()。
3Choreographer计算掉帧逻辑。
4Choreographer处理Input回调。
5Choreographer处理Animation回调。
6Choreographer处理Insets Animation回调。
7Choreographer处理Traversal回调。
8Choreographer处理Commit回调。
9RenderThread处理绘制数据,执行渲染。
10RenderThread将处理好的Buffer提交给SurfaceFlinger进行合成。
这一块的流程我们会在下面展开讲。
TripleBuffer
Triple Buffer简单说来是使用三个Buffer进行轮转,如下所示:
我们一直提到Buffer,那什么是Buffer?
Buffer/FrameBuffer是内核系统提供给图形硬件的抽象描述,之所以称为buffer,是因为他占用了系统存储空间的一部分,是一块包含屏幕显示信息的缓冲区。简单来说FrameBuffer代表了屏幕即将要显示的一帧画面。
在Android系统中,FrameBuffer提供的设备节点是/dev/graphics/fb*,fb按照顺序排列,支持多个屏幕,例如fb0表示主屏幕。而负责管理FrameBuffer缓冲区的正是Gralloc,它提供了对FrameBuffer缓冲区统一的管理和访问。
Gralloc通过一种BufferQueue的机制来管理FrameBuffer缓冲区。
什么是BufferQueue?
BufferQueue是渲染数据流经的通道,一般由消费者创建,而生产者一般不和BufferQueue在同一个进程里,如图所示这里的消费者就是SurfaceFlinger,生生产者有多个来源。
图形
BufferQueue工作流如下所示:
图形
1dequeue:生产者发起,当生产者需要缓冲区时,它会通过调用dequeueBuffer()向BufferQueue请求一个可用的缓冲区,并指定缓冲区的宽度、高度、像素格式和使用标记。
2queue:生产者发起,当生产者需要填充缓冲区时,它会通过调用queueBuffer()将缓冲区返回到BufferQueue。
3acquire:消费者发起,消费者通过acquireQueue()从BufferQueue获取缓冲区并使用缓冲区内的内容。
4release:消费者发起,消费者操作完成以后,通过调用releaseBuffer()将该缓冲区返回到BufferQueue。
Rendering Architecture
UI Framework
上图的右边显示了一个基本的Android页面包含哪些元素,直观上看一个Android界面至少由Activity、Window、View三部分构成,它们相互陪配合渲染出可供用户交互的图形界面。
Activity、Window、View三者关联的类图如下所示:
这张类图大体可以分为Activity、Window、View、WMS四大块构成。
●Activity:图形系统的*容器,继承自ContextThemeWrapper,直接面向用户,负责页面的生命周期管理,接收用户的输入。
○【变量关联-上下文】Activity组件在启动的时候,系统会为它创建一个ContextImpl对象,通过Activity.attach()方法关联到Activity,并通过ContextThemeWrapper.attachBaseContext()和ContextWrapper.attachBaseContext()方法关联到它们的mBase变量;系统还会调用ContextImpl.setOuterContext()方法来将Activity组件关联到其成员变量mOuterContext上。
○【变量关联-窗口】Activity的成员变量mWindow指向Window,而Window的成员变量mContext和mCallback都指向了Activity。
●Window:图形系统的窗口,它的主要功能是描述窗口信息,用来管理View。Window的实际实现类是PhoneWindow,由PolicyManager.makeNewWindow()创建。
○【变量关联-视图管理】Window的成员变量mWindowManager指向WindowManagerImpl对象,该对象内部有个WindowManagerGlobal,它也是实际的实现类,该类内部维护了VIew、VIewRootImpl和LayoutParams三个数组,用来管理View。
○【变量关联-视图容器】PhoneWIndow内部有两个关键的变量DecorView mDecor和ViewGroup mContentParent,mDecor是应用的*视图,mContentParent是视图的父容器,一般是mDrcor自身或者是它的子视图。
●View:图形界面的视图,它内部的draw()方法用来绘制视图,而控制绘制的是ViewRootImpl对象,它继承于Handler,
○【变量关联-绘制控制器】ViewRootImpl内部的mView变量指向的就是DecorView,它会调用DecorView.draw()方法来控制绘制。
●ViewRootImpl:图形系统的绘制控制器,每一个Activity组件都由一个对应的ViewRootImpl对象、*VIew对象、和WIndowManager.LayoutParams对象。这也是WindowManagerGlobal里三个数组的来源。ViewRootImpl一方面利用内部对象sWindowSession与WMS进行双向通信,另一方面,ViewRootImpl作为Handler的实现类,还负责向主线程发送消息。
○【消息分发-输入事件】当InputManager接收到键盘、触摸屏等输入事件事,ViewRootImpl会把这些事件封装成一个消息,发送到主线程中进行处理。
○【消息分发-绘制事件】当需要重新绘制它关联的一个View时,ViewRootImpl会把绘制操作封装成一个消息发送到主线程中进行处理。
●Surface:图形系统的连接器,每个ViewRootImpl对象内部都有个Surface对象,这个Java层的Surface对象,其成员变量mNativeSurface指向了一个C++层的Surface对象。C++层的Surface对象负责向应用窗口的图形缓冲区填充UI数据,即设置窗口的纹理,这些纹理保存中Surface类的成员变量mCanvas中,即通过这个画布就可以访问图形缓冲区。
●Canvas:图形系统的画布,ViewRootImpl在执行绘制指令时,会先获取Surface的Canvas画布对象,然后传递给VIew.draw()方法,然后就可以在Canvas上进行绘制了。实际的绘制工作是由底层的Skia图形库完成的。
●WindowManager:Window需要与WMS通信,但是它并没有直接实现这个功能,因为一个应用中可能存在多个Window,每个Window需要与WMS单独通信,会造成资源浪费和管理混乱,因此这是便有了WindowManager,它也是Window内部的成员变量mWindowManager。
●WindowManagerImpl:WindowManager的子类,它内部有一个WindowManagerGlobal对象,负责管理View、ViewRootImpl和LayoutParams的数组对象。
●WindowManagerService:窗口管理服务,负责创建和管理WIndow。
以上便是Android的UI Framework,了解大致的构成,有助于我们了解后面的流程。
Structure
从结构上看,Android的渲染时UI Thread和Render Thread相互配合完成的。
●UI Thread:UI Thread负责生成绘制指令,同步给Render Thread。
●Render Thread:Render Thread对这些绘制指令,调用OpenGL方法,将生产的Frame Buffer提供给SurfaceFlinger进行合成,最终输出到屏幕上显示。
Flow
从流程上看,Android UI系统在VSync信号的驱动下有条不紊的进行绘制和合成的工作。VSYNC_APP信号控制着App端的绘制,源源不断的生产buffer数据,VSYNC_SF信号则控制着SurfaceFlinger消费buffer数据,合成上屏。
流程图如下所示:
1App端第一N次接收SYNC-app信号后,回调Choreographer.onVsync()方法开始App的第N帧的渲染。开始依次处理Input、Animate、Traversal(Measure、Layout、Draw)等回调。生产绘制指令。
2App端完成这一帧的绘制后,会将绘制指令保存在Buffer中,放入BufferQueue。这样当前缓存的帧数据就是加一。
3SurfaceFlinger端第N次接收到SYNC-sf信号后,开始处理这一帧的合成工作,消费掉这一帧的数据,当前缓存的帧数据就会减一。
以上便是一帧数据的生产与消费的过程,App端和SurfaceFlinger端一前一后,井然有序的生产和消费者帧数据,进行界面的渲染。
注:理想情况下,这种一前一后的方式看起来没问题。真实的情况是SurfaceFlinger需要buffer数据的时候,App端可能还没渲染好,这样就可以出现掉帧(jank),所以Android引入了Tripple Buffer等缓存机制,这个我们后面会讲。
以上面三个流程可以做进一步抽象:
1UI Thread进行绘制,并将数据同步给Render Thread。
2Render Thread将buffer数据提交给SurfaceFlinger。
3SurfaceFlinger/HW进行合成上屏。
前两步可以归为Rendering Pipeline,后两步可以归为Graphics Pipeline。我们接着来看。
Rendering Pipeline
Rendering Pipeline由UI Thread和Render Thread配合完成,如下所示:
App端在VSYNC-app信号的驱动下开始进行绘制工作,整个流程由两大线程配合工作,核心入口如下:
●UI Thread:Choreographer.doFrame()
●Render Thread:DrawFrame
具体流程如下:
1input,处理Input回调。
2animation,处理Animation回调。
3traversal,处理Traversal回调,它会相继调用Measure(测量)、Layout(布局)、Draw(构建DisplayList,里面包含OpenGL渲染所需的命令与数据)。
4syncFrameState,主线程与渲染线程同步(sync)绘制信息。
5dequeue buffer:从SurfaceFlinger的BufferQueue取出一个buffer(两者不在同一进程,因而它是一个Binder调用),调用OpenGL相关函数,执行真正的渲染操作,最后这个渲染好的buffer会通过queue buffer放回BufferQueue。
6flush draw commands,渲染线程执行绘制操作,生成OpenGL指令。
7eglSwapBufferWIthDamageKHR:调用queue buffer将刚刚取出的buffer放回BufferQueue,供SurfaceFlinger消费。
在这个流程的前半段,Choreographer.doFrame()的前半段,它优先处理了三个回调:
●Input:用户输入
●Animation:动画
●Traversal:绘制
这三个回调是由优先级的,优先级就是按照他们的执行顺序排布的,也就是说Android系统中,响应用户输入是第一位的,接着是动画,最后才是绘制。这个在Flutter等系统中也是一样的。
Traversal回调由Choreographer.doFrame()发起,最终在ViewRootImpl.performTraversal()中被执行,它包含了Android UI系统的三个核心操作:
●Measure:测量View及其子View的大小。
●Layout:测量View及其子View的位置。
●Draw:绘制View及其子View
了解了Rendering Pipeline中关键的步骤,我们分别来看看它们的实现。
1 Input
处理用户输入回调。
2 Animation
处理动画回调。
3 Measure
在分析Measure流程之前,我们先来了解一下Measure、Layout、Draw的整体流程。
我们接着来说Measure。
Measure是用来测量View及其子View的大小。View是构成一个Android界面的基本元素。
View是一个矩形区域,它有自己的位置、大小与边距,如下所示:
Link
●Position:有左上角坐标(getLeft(), getTop())决定,该坐标是以它的父View的左上角为坐标原点,单位是pixels。
●Size:View的大小有两对值来表示。getMeasuredWidth()/getMeasuredHeight()这组值表示了该View在它的父View里期望的大小值,在measure()方法完成后可获得。getWidth()/getHeight()这组值表示了该View在屏幕上的实际大小,在draw()方法完成后可获得。
●Padding:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理padding,否则它不会生效,这一块的内容我们会在View自定义实践系列的文章中展开。
●Margin:View的外边距用margin来表示,它表示View的边缘离它相邻的View的距离。Measure过程决定了View的宽高,该过程完成后,通常都可以通过getMeasuredWith()/getMeasuredHeight()获得宽高。
测量的过程便是为了计算这些数据,在做测量的时候,measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。对于onMeasure(),View和ViewGroup有所区别:
●View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
●ViewGroup:ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会根据子View的尺寸和位置来计算出自己的尺寸然后保存。
MeasureSpec,它是一个32位int值。
●高2位:SpecMode,测量模式
○UNSPECIFIED:父View不对子View做任何限制,需要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
○EXACTLY:父View已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应match_parent和指定大小这两种情况。
○AT_MOST:父View给子VIew提供一个最大可用的大小,子View去自适应这个大小。它对于wrap_content这种情况。
●低30位:SpecSize,在特定测量模式下的大小。
4 Layout
在进行布局的时候,layout()方法被父View调用,在layout()中它会保存父View传进来的自己的位置和尺寸,并且调用onLayout()来进行实际的内部布局。对于onLayout(),View和ViewGroup有所区别:
●View:由于没有子 View,所以 View 的 onLayout() 什么也不做。
●ViewGroup:ViewGroup在onLayout()中会调用自己的所有子View的layout()方法,把它们的尺寸和位置传给它们,让它们完成自我的内部布局。
Layout流程就会涉及到一些布局算法了,Android上常用的布局类型如下所示:
●LinearLayout
●AdapterView
●RelativeLayout
●ConstraintLayout
●MotionLayout
它们都继承于ViewGroup。
5 Draw
Android硬件加速是默认开启的,因而Draw方法并没有执行真正的绘制调用,而是把要绘制的内容记录到DisplayList里面,然后同步到RenderThread中,一般同步完成,UI Thread就可以被释放出来做其他事情,而Render Thread继续执行渲染工作。
View的绘制流程如下所示:
1Draw the background
2If necessary, save the canvas' layers to prepare for fading
3Draw view's content
4Draw children
5If necessary, draw the fading edges and restore layers
6Draw decorations (scrollbars for instance)
引自View.java
这些绘制最终都会调用到View.invalidate()和View.invalidate()两个方法。
6 Sync
接来下这块会在Render Thread中执行,相关代码如下:
●frameworks/base/libs/hwui/renderthread/
这部分代码会运行在Render Thread下,它会执行一个DrawFrameTask,这个Task的核心方法就是DrawFrame,DrawFrame会执行一系列操作,如下所示:
1
这里首先会调用syncFrameState,在主线程与渲染线程之间同步(sync)绘制信息。
然后调用flush draw commands,渲染线程将数据上传(ARM设备内存一般是CPU和GPU共享内存)给GPU。并通知SurfaceFlinger进行图层合成。
从源码的角度看图形数据流的流向。
Graphics Pipeline
Android也是通过同步光栅化的方式进行栅格化和合成上屏。什么是同步光栅化?
同步光栅化
光栅化和合成在一个线程,或者通过线程同步等方式来保证光栅化和合成的的顺序。
:
●直接光栅化:直接执行可见图层的DisplayList中可见区域的绘制指令进行光栅化,在目标Surface的像素缓冲区上生成像素的颜色值。
●间接光栅化:为指定图层分配额外的像素缓冲区(例如Android提供View.setLayerType允许应用为指定View提供像素缓冲区,Flutter提供了Relayout Boundary机制来为特定图层分配额外缓冲区),该图层光栅化的过程中会先写入自身的像素缓冲区,渲染引擎再将这些图层的像素缓冲区通过合成输出到目标Surface的像素缓冲区。
异步分块光栅化
图层会按照一定的规则粉尘同样大小的图块,光栅化以图块为单位进行,每个光栅化任务执行图块区域内的指令,将执行结果写入分块的像素缓冲区,光栅化和合成不在一个线程内执行,并且不是同步的。如果合成过程中,某个分块没有完成光栅化,那么它会保留空白或者绘制一个棋盘格图形。
Android和Flutter采用同步光栅化策略,以直接光栅化为主,光栅化和合成同步执行,在合成的过程中完成光栅化。而Chromium采用异步分块光栅化测量,图层会进行分块,光栅化和合成异步执行。
9 Raster&Compositing
App端生产的FrameBuffer数据会在SurfaceFlinger/HW端进行消费,两个通过BufferQueue来传递数据。
上面提到了两个角色:
●SurfaceFlinger:负责接收多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。
●HWCompositor:
具体流程如下:
SurfaceFinger主线程接收到VSYNC-sf信号以后,开始合成图层,如果之前的GPU渲染任务还没有结束,则等待GPU渲染完成再进行合成(Fence机制)。处理合成的主要是主线程里的onMessageReceived(),它会处理以下消息:
1执行handleMessageTransaction()和handleMessageInvalidate()方法,处理MessageQueue::INVALIDATA消息。
2执行handleMessageRefresh()方法,处理MessageQueue::Refresh消息。
○准备工作:preComposition() -> rebuildLayerStacks() -> calculateWorkingSet()
○合成工作:beiginFrame() -> prepareFrame() -> doDebugFlashRegions() -> doComposition()
○收尾工作:logLayerStats() -> postFrame() -> postComposition()
SurfaceFlinger在合成的时候会将一些合成工作委托给Hardware Compositer,降低GPU的负担。合成好的数据放到屏幕对应的Frame Buffer中,屏幕依赖着自己的刷新频率进行刷新,整个页面就显示在屏幕上了。