2.1.1 绘制原理
绘制任务是由应用发起的,最终通过系统层绘制到硬件屏幕上,也就是说,应用进程绘制好后,通过跨进程通信机制把需要显示的数据传到系统层,由系统层中的SurfaceFlinger服务绘制到屏幕上。那么应用层和系统层中的流程是什么样的呢?接下来将进行具体介绍。
1. 应用层
先来看一个UI界面的典型构成框架,也可以是一个Activity的构成。如图2-2所示,有很多不同层次的基本元素——View,整体是一个树型结构,有不同的嵌套,存在着父子关系,子View在父View中,这些View都经过一个相同的流程最终显示到屏幕上,这也意味着要完整地显示所有数据,就要对其中的View都进行一次绘制工作,并且针对每个View的操作都是一个递归过程。
图2-2 页面构成框架
在Android的每个View绘制中有三个核心步骤(见图2-3),通过Measure和Layout来确定当前需要绘制的View所在的大小和位置,通过绘制(Draw)到surface,在Android系统中整体的绘图源码是在ViewRootImp类的performTraversals()方法,通过这个方法可以看出Measure和Layout都是递归来获取View的大小和位置,并且以深度作为优先级。可以看出,层级越深,元素越多,耗时也就越长。
图2-3 View绘制流程
(1)Measure
用深度优先原则递归得到所有视图(View)的宽、高;获取当前View的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。如果当前正在测量的子视图child是一个视图容器,那么它又会重复执行操作,直到它的所有子孙视图的大小都测量完成为止。
(2)Layout
用深度优先原则递归得到所有视图(View)的位置;当一个子View在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中确定的宽度和高度,就可以完全确定它在应用程序窗口中的布局。
(3)Draw
目前Android支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),其中硬件加速在Android 3.0开始已经全面支持,很明显,硬件加速在UI的显示和绘制的效率远远高于CPU绘制,但硬件加速并非如大家所想的那么完善,它也存在明显的缺点:
□ 耗电:GPU的功耗比CPU高。
□ 兼容问题:某些接口和函数不支持硬件加速。
□ 内存大:使用OpenGL的接口至少需要8MB内存。
所以是否使用硬件加速,需要考虑一些接口是否支持硬件加速,同时结合产品的形态和平台,比如TV版本就不需要考虑功耗的问题,而且TV屏幕大,使用硬件加速容易实现更好的显示效果。
2. 系统层
真正把需要显示的数据渲染到屏幕上,是通过系统级进程中的SurfaceFlinger服务来实现的,SurfaceFlinger的具体实现和工作原理因为和应用层关系不大,所以这里不做过多介绍,只需要了解它主要是做些什么工作。
□ 响应客户端事件,创建Layer与客户端的Surface建立连接。
□ 接收客户端数据及属性,修改Layer属性,如尺寸、颜色、透明度等。
□ 将创建的Layer内容刷新到屏幕上。
□ 维持Layer的序列,并对Layer最终输出做出裁剪计算。
既然是两个不同进程,那么肯定需要一个跨进程的通信机制来实现数据传输,在Android的显示系统,使用了Android的匿名共享内存:SharedClient,每一个应用和SurfaceFlinger之间都会创建一个SharedClient,如图2-4所示。从图2-4中可以看出,在每个SharedClient中,最多可以创建31个SharedBufferStack,每个Surface都对应一个SharedBufferStack,也就是一个window。
图2-4 Android显示框架
一个SharedClient对应一个Android应用程序,而一个Android应用程序可能包含多个窗口,即Surface。也就是说SharedClient包含的是SharedBufferStack的集合。因为最多可以创建31个SharedBufferStack,这也意味着一个Android应用程序最多可以包含31个窗口,同时每个SharedBufferStack中又包含了两个(低于4.1版本)或者三个(4.1及以上版本)缓冲区,即在后面的显示刷新机制中会提到的双缓冲和三重缓冲技术。
最后总结起来显示整体流程分为三个模块:应用层绘制到缓存区,SurfaceFlinger把缓存区数据渲染到屏幕,由于是两个不同的进程,所以使用Android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。
SurfaceFlinger把缓存区数据渲染到屏幕(流程如图2-5所示),主要是驱动层的事情,这里不做太多解释。
图2-5 渲染数据流程图
从图2-5中可以看出,绘制过程首先是CPU准备数据,通过Driver层把数据交给CPU渲染,其中CPU主要负责Measure、Layout、Record、Execute的数据计算工作,GPU负责Rasterization(栅格化)、渲染。由于图形API不允许CPU直接与GPU通信,而是通过中间的一个图形驱动层(Graphics Driver)来连接这两部分。图形驱动维护了一个队列,CPU把display list添加到队列中,GPU从这个队列取出数据进行绘制,最终才在显示屏上显示出来。
知道了绘制的原理后,那么到底绘制一个单元多长时间才是合理的,首先需要了解一个名词:FPS。FPS(Frames Per Second)表示每秒传递的帧数。在理想情况下,60 FPS就感觉不到卡,这意味着每个绘制时长应该在16ms以内,如图2-6所示。
图2-6 理想状态下的绘制操作
但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的60FPS。即为了实现60FPS,就意味着程序的大多数绘制操作都必须在16ms内完成。
如果某个操作花费的时间是24ms,系统在得到VSYNC信号时就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。主要场景在执行动画或者滑动ListView时更容易感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致CPU或者GPU负载过重从而出现丢帧现象:可能是你的Layout太过复杂,无法在16ms内完成渲染;可能是UI上有层叠太多的绘制单元;还有可能是动画执行的次数过多。
最终的数据是刷新机制通过系统去刷新数据,刷新不及时也是引起卡顿的一个主要原因。接下来将详细介绍系统是怎么刷新的以及在什么情况下会导致卡顿发生。