Android优化——绘制优化之android系统显示原理(一)

1、android系统显示原理

能够简单归纳为:android应用程序把通过测量、布局、绘制后的surface缓存数据,经过SurfaceFlinger把数据渲染到显示屏幕上,经过android的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,经过进程间通讯把应用层须要绘制的数据传递到系统层服务,系统层服务经过刷新机制把数据更新到屏幕。java

android的图形显示系统采用的是Client/Server架构。SurfaceFlinger(Server)由C++代码编写。Client端代码分为两部分,一部分由java提供给应用层使用的API,另外一部分则是由C++写成的底层具体实现。android

一、基本概念

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.1 应用层

在android的每一个view绘制中有三个核心步骤:经过Measure和Layout来肯定当前须要绘制的view所在的大小和位置,经过绘制(draw)到surface,在android系统中总体绘图源码是在ViewRootImp类的performTraversals()方法,经过这个方法能够看出Measuret Layout都是递归来获取view的大小和位置,而且以深度做为优先级。由此能够看出,层级越深,元素越多,耗时也就越长。View绘制流程为:Measure-->Layout-->Draw。动画

2.1.一、Measure

用深度优先原则递归获得全部视图的宽、高;获取当前View的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec以后,能够调用它的成员函数Measure来设置它的大小。若是当前正在测量的视图是一个容器,那么它又会重复执行操做,直到它的全部子孙视图大小都测量完成为止。线程

2.1.二、Layout

用深度优先原则递归获得全部视图的位置;当前一个子view在应用程序窗口左上角的位置肯定后,再结合它在前面测量获得的宽度和高度,就能够彻底肯定他在应用程序窗口中的布局。调试

2.1.三、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,来实现跨进程的数据传输。

一、每一个应用和SurfaceFlinger之间都会建立一个SharedClient,一个应用对应一个SharedClient。

二、SharedClient包含的是SharedBufferStack的集合,每一个SharedClient中最多建立31个SharedBufferStack。

三、每一个SharedBufferStack都对应一个Surface,也就是一个Window,这意味着一个android应用程序最多能够包含31个窗口。

四、每一个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的由来。

三、刷新机制

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中断,不能及时处理,也是徒劳无功。

四、卡顿的根本缘由

那卡顿的根本缘由是什么呢,从android系统的显示原理中能够看到,影响绘制的根本缘由有如下两方面:

绘制任务过重,绘制一帧内容耗时太长。 主线程太忙了,致使VSync信号来时尚未准备好数据致使丢帧。 耗时太长,须要从UI布局和绘制上来具体分析。这里主要讨论下第二个方面。咱们知道全部的绘制工做都是由主线程,也就是UI线程来负责,主线程的关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据。在android应用开发中 ,特别须要避免任何阻碍主线程的事情,这样应用程序才能保持对用户操做的即时响应。

在实际的开发过程当中,咱们须要知道主线程应该作什么,总结起来主线程主要作如下几个方面工做:

  • UI生命周期控制
  • 系统事件处理
  • 消息处理
  • 界面布局
  • 界面绘制
  • 界面刷新 除了这些之外,尽可能避免将其余处理放到主线程中,特别是复杂的数据计算和网络请求。
相关文章
相关标签/搜索