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

栏目: Android · 发布时间: 6年前

内容简介:可以简单概括为: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生命周期控制
  • 系统事件处理
  • 消息处理
  • 界面布局
  • 界面绘制
  • 界面刷新 除了这些以外,尽量避免将其他处理放到主线程中,特别是复杂的数据计算和网络请求。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Haskell School of Expression

The Haskell School of Expression

Paul Hudak / Cambridge University Press / 2000-01 / USD 95.00

Functional programming is a style of programming that emphasizes the use of functions (in contrast to object-oriented programming, which emphasizes the use of objects). It has become popular in recen......一起来看看 《The Haskell School of Expression》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试