内容简介:可以简单概括为:android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到显示屏幕上,通过android的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。android的图形显示系统采用的是Client/Server架构。SurfaceFlinger(Server)由C++代码编写。Client端代码分为两部分,一部分由java提供给应用层使用
可以简单概括为:android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到显示屏幕上,通过android的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。
android的图形显示系统采用的是Client/Server架构。SurfaceFlinger(Server)由C++代码编写。Client端代码分为两部分,一部分由 java 提供给应用层使用的API,另一部分则是由C++写成的底层具体实现。
1、基本概念
CPU: 中央处理器,它集成了运算、缓冲、控制等单元,包括绘图功能。CPU将对象处理为多维图形,纹理(Bitmaps、Drawables等都是一起打包到统一的Texture纹理)。
GPU:一个类似于CPU的专门用来处理Graphics的处理器, 作用用来帮助加快格栅化操作,当然,也有相应的缓存数据(例如缓存已经光栅化过的bitmap等)机制。
DisplayList:它相当于是从View的绘制命令到GL命令之间的“中间语言”。它记录了绘制该View所需的全部信息,之后只要重放(replay)即可完成内容的绘制。这样如果View没有改动或只部分改动,便可重用或修改DisplayList,从而避免调用了一些上层代码,提高了效率。
栅格化:是将图片等矢量资源,转化为一格格像素点的像素图,显示到屏幕上。
FPS(Frames Per Second):表示每秒传递的帧数。通俗来讲就是指动画或视频的画面数,对应的就是APP UI界面的刷行频率,在一个UI动画的播放过程中,FPS越大,界面表现越流畅,FPS越低,界面表现越卡顿。
2、绘制原理
2.1 应用层
在android的每个view绘制中有三个核心步骤:通过Measure和Layout来确定当前需要绘制的view所在的大小和位置,通过绘制(draw)到surface,在android系统中整体绘图源码是在ViewRootImp类的performTraversals()方法,通过这个方法可以看出Measuret Layout都是递归来获取view的大小和位置,并且以深度作为优先级。由此可以看出,层级越深,元素越多,耗时也就越长。View绘制流程为:Measure-->Layout-->Draw。
2.1.1、Measure
用深度优先原则递归得到所有视图的宽、高;获取当前View的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。如果当前正在测量的视图是一个容器,那么它又会重复执行操作,直到它的所有子孙视图大小都测量完成为止。
2.1.2、Layout
用深度优先原则递归得到所有视图的位置;当前一个子view在应用程序窗口左上角的位置确定后,再结合它在前面测量得到的宽度和高度,就可以完全确定他在应用程序窗口中的布局。
2.1.3、Draw
分为两种绘制方式:软件绘制(CPU)和硬件加速(GPU),其中硬件加速在android3.0开始已经全面支持,很明显,硬件加速在UI的显示及绘制上效率远高于CPU绘制,但也有一些缺点:
-
耗电:GPU的功耗比CPU高。
-
兼容问题:某些接口和函数不支持硬件加速。
-
内存大:使用OPenGL的接口至少需要8MB内存。
2.2系统层
2.2.1 SurfaceFlinger服务
真正把需要显示的数据渲染到屏幕上,是通过系统级进程中的SurfaceFlinger服务来实现的。它的主要工作有:
- 响应客户端事件,创建Layer与客户端的Surface建立连接
- 接收客户端数据及属性,修改Layer属性,如尺寸、颜色、透明度等。
- 将创建的Layer内容刷新到屏幕上。
- 维持Layer的序列,并对Layer最终输出做出裁剪计算。
在android的显示系统中使用了android的匿名共享内存:SharedClient,来实现跨进程的数据传输。
1、每个应用和SurfaceFlinger之间都会创建一个SharedClient,一个应用对应一个SharedClient。
2、SharedClient包含的是SharedBufferStack的集合,每个SharedClient中最多创建31个SharedBufferStack。
3、每个SharedBufferStack都对应一个Surface,也就是一个Window,这意味着一个android应用程序最多可以包含31个窗口。
4、每个SharedBufferStack中包含两个(低于4.1版本)或者三个(4.1及以上版本)缓冲区,即后面显示刷新机制中提到的双缓冲和三重缓冲技术。
最后总起来显示整体流程分三个模块:应用层绘制到缓存区;SurfaceFlinger把缓存区数据渲染到屏幕;由于是两个不同的进程,所以使用android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。
绘制过程首先是CPU准备数据,通过Driver层把数据交给GPU渲染,其中CPU负责Measure、Layout、Record、Execute的数据计算工作,GPU负责栅格化、渲染。由于图形API不允许CPU直接与GPU通信,而是通过中间的一个图形驱动层(Graphics Driver)来连接两部分。图形驱动维护了一个队列,CPU把DisplayList添加到队列中,GPU从这个队列取出数据进行绘制,最终才在显示屏上显示出来。
2.2.2 60Hz 和 16 ms
12 FPS——由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10-12帧的时候,就会认为是连贯的。
24 FPS——有声电影的拍摄及播放帧率均为每秒24帧,对一般人而言已算可接受。
60 FPS—— 在与手机交互过程中,如触摸和反馈60帧以下人是能感觉出来的。60帧以上不能察觉变化,当帧率低于60FPS 时感觉的画面的卡顿和迟滞现象。
由于人体眼睛生理结构的特殊性,于是这就是60Hz的由来,而1000ms/60=16.66ms这就是16ms的由来。
3、刷新机制
Android系统每隔16ms发出VSync信号,触发对UI进行渲染(即每16ms显示一帧),如果每次渲染都成功这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着计算渲染的大多数操作都必须在16ms内完成。如果某个操作花费时间是24ms,系统在得到VSync信号时就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一幅画面,从而感觉卡顿。有很多原因可以导致CPU或者GUP负载过重从而出现丢帧现象:可能是Layout太过复杂,无法在16ms内完成渲染;可能是UI上有层叠太多的绘制单元;还有可能是动画执行次数过多。
在android4.1版本中有效处理了UI流畅性差的问题。其解决方法即在4.1版本推出的Project Buffer。Project Buffer对android Display系统进行了重构,引入三个核心元素:VSync、Triple Buffer和Choreographer。其中VSync是理解Project Buffer的核心,,简单地可以把它认为是一种定时中断技术。Choreographer起调试的作用,将绘制工作统一到VSync的某个时间点上,使应用的绘制工作有序。
双缓冲:显示内容的数据内存。我们知道在 Linux 上通常使用Framebuffer来做显示输出,当用户进程更新Framebuffer中的数据后,显示驱动会把Framebuffer中每个像素点的值更新到屏幕,但是这样会有一个问题,如果上一帧数据还没显示完,Framebuffer中的数据又更新了,就会带来残影问题,给用户的直观感觉就会有闪烁感,所以普遍采用了双缓冲技术。双缓冲意味着要使用两个缓冲区(在SharedBufferStack中),其中一个称为Front Buffer,另一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。即只有当另一个buffer的数据准备好后,通过io_ctrl来通知显示设备切换buffer。
VSync(Verical Synchronization):垂直同步,从前面的双缓冲介绍中可以了解到,只有当另一个buffer准备好后,才能通知刷新 ,这就需要CPU以主动查询的方式来保证数据是否准备好,因为这种机制效率很低,所以引入了VSync。可以简单地把它认为是一种定时中断,一旦收到VSync中断,CPU就开始处理各帧数据。 Choreographer:收到VSync信号时,调用 用户设置的回调函数。一共有以下三种类型的回调:
-
CALLBACK_INPUT:优先级最高,与输入事件有关。
-
CALLBACK_ANIMATION:第二优先级,与动画有关。
-
CALLBACK_TRAVERSAL:最低优先级,与UI控件绘制有关。
接下来通过时序图来分析刷新的过程,这些时序图是2018年Google I/O讲解新的显示系统提供的,图3.1所示的时序图有三个元素:Display(显示设备),CPU-CPU准备数据,GPU-GPU准备数据。最下面的显示时间,根据理想的60FPS,以16ms为一个显示周期。
图3.1 没有Vsync信息的刷新
(1)没有VSnyc信号同步
我们以16ms为单位来进行分析:
1)从第一16ms开始看,Display显示第0帧,CPU处理完第一帧后GPU紧接其后处理第一帧。三者都在正常工作。
2)时间进入第二个16ms:因为在上一个16ms时间内,第1帧已经由CPU和GPU处理完毕。所以Display可以正常显示第1帧。显示没有问题,但在本16ms期间,CPU和GPU并未及时绘制第2帧数据(前面的空白区在忙别的事情),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
3)时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第1帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank),这就导致错过了显示第2帧。
通过上述分析可知,在第二个16ms时,发生Jank的关键问题在于,为何在第1个16ms段内,CPU/GPU没有及时处理第2帧数据?从第2个16ms开始有一段空白的时间,可以说明原因所在,那就是CPU可能是在忙别的事情 ,不知道该到处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了。为解决这个问题,4.1版本推出了Project Buffer,核心目的就是解决刷新不同步的问题。
(2)有VSync信号同步
加入VSync后,从图3.2可以看到,一旦收到VSync中断,CPU就开始处理各帧的数据。大部分的android显示设备刷新率是60Hz,这也就意味着第一帧最多只能有1/60=16ms左右的准备时间。假如CPU/GPU的FPS高于这个值,显示效果将更好。但是,这时又出现一个新问题:CPU和GPU处理数据的速度都能在16ms内完成,而且还有时间空余,但必须等到VSync信号到来后,才处理下一帧数据,因此CPU/GPU的FPS被拉低到与Display的FPS相同。
从图3.3采用双缓冲区的显示效果来看:在双缓冲下,CPU/GPU的FPS大于刷新频率同时采用了双缓冲技术以及VSync,可以看到整个过程还是相当不错的,虽然CPU/GPU处理所用的时间时短时长,但总体来说都在16ms内,因而不影响显示效果。A和B分别代表两个缓冲区,它们不断交换来正确显示画面。但如果CPU/GPU的FPS小于DIsplay的FPS,情况又不同了,如图3.4所示。
图3.2 有VSnyc的绘制
图3.3 双缓冲下的时序图
图3.4 双缓冲下CPU/GPU的FPS小于刷新频率的时序图
从图3.4可以看到,当CPU/GPU的处理时间超过16ms时,第一个VSync就已经到来,但缓冲区B中的数据却还没有准备好,这样就只能继续显示之前A缓冲区中的内容。而后面B完成后,又因为还没有VSync信号,CPU/GPU这个时候只能等待下一个VSync的来临才开始处理下一帧数据。因此在整个过程中,有一大段时间被浪费。总结这段话就是:
1)在第2个16ms时间段内,Display本就显示B帧,但因为GPU还在处理B帧,导致A帧被重复显示。
2)同理,在第动起来个16ms时间段内,CPU无所事事,因为A Buffer由Display的使用。B Buffer由GPU使用。注意,一旦过了VSync时间点,CPU就不能被触发以及处理绘制工作了。
为什么CPU不能在第2个16ms时间处即VSync到来就开始工作呢?很明显,原因就是只有两个Buffer。如果有第三个Buffer存在,CPU就可以开始工作,而不至于空闲。于是在android4.1以后,引出了第三个缓冲区:Triple Buffer。Triple Buffer利用CPU/GPU的空闲等待时间提前准备好数据,并不一定会使用。
引入Triple Buffer后的刷新时序如图3.5所示。
图3.5 使用Triple Buffer时序图
在第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示一次A帧,但后续显示就比较顺畅了。是不是Buffer越多越好呢?回答是否定的。由图3.5可知,在第二个时间段内,CPU绘制的和C帧数据要到第四个16ms才显示,这比双缓存情况多了16ms延迟。所以缓冲区不是越多越好,要做到平衡到最佳效果。
从以上分析来看,andorid系每户在显示机制上解决了android UI显示不流畅的问题,并且从Google 2012年I/O大会给出的视频来看,其效果也达到了预期。但实际在应用开发过程中仍然存在卡顿的现象。因为VSync中断处理的线程优先级一定要最高,否则即使接收到VSync中断,不能及时处理,也是徒劳无功。
4、卡顿的根本原因
那卡顿的根本原因是什么呢,从android系统的显示原理中可以看到,影响绘制的根本原因有以下两方面:
绘制任务太重,绘制一帧内容耗时太长。 主线程太忙了,导致VSync信号来时还没有准备好数据导致丢帧。 耗时太长,需要从UI布局和绘制上来具体分析。这里主要讨论下第二个方面。我们知道所有的绘制工作都是由主线程,也就是UI线程来负责,主线程的关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据。在android应用开发中 ,特别需要避免任何阻碍主线程的事情,这样应用程序才能保持对用户操作的即时响应。
在实际的开发过程中,我们需要知道主线程应该做什么,总结起来主线程主要做以下几个方面工作:
- UI生命周期控制
- 系统事件处理
- 消息处理
- 界面布局
- 界面绘制
- 界面刷新 除了这些以外,尽量避免将其他处理放到主线程中,特别是复杂的数据计算和网络请求。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 重温View绘制原理(二)
- 【 可视化】热力图绘制原理
- ViewGroup 默认顺序绘制子 View,如何修改?什么场景需要修改绘制顺序?
- Shader 绘制基础图形
- css绘制特殊图形
- View 绘制流程分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。