Android性能优化典范-底层工作原理
60fps的由来:
作为程序员,我们经常会听到60fps和16ms这两个重要值,同时我们会将程序是否达到60fps来作为App性能的衡量标准,这是因为人眼与大脑之间的感知对60fps的画面更新是最为流畅顺滑的。
单纯的列出数据,可能无法帮助大家进行理解,这里我们举几个实际生活中常用的例子:
a. 12fps类似于手动快速翻书的频率
b. 24fps则可以满足人眼感知的连续线性的运动,这归功于运用模糊的效果,电影胶圈就是以这个帧率作为破防帧率的。
c. 但是30fps但是低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果。我们在开发app的时候需要保持的性能目标就为60fps,换算为时间值也就是:1000ms/60大概为16ms,也就意味着我们需要在16ms内处理完所有的任务。
卡顿的由来:
大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元素来实现流畅的用户体验。但是Android系统很有可能无法及时在16ms内完成那些复杂的界面渲染操作。举个例子来说,理想情况下,Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps:
理想情况下的渲染而现实情况下,总会因为各种原因造成16ms内无法运算出绘制时所需要的运算结果,这样就会造成以下的情况:
假如一个操作花费时间为24ms,则此时在16ms(即系统在接收到VSYNC信号指令)时,无法进行渲染,则这种情况下,只能在接收到下一个VSYNC信号时才进行渲染。这样,在中间的16ms区间内只能显示之前的一帧,这样就意味着在32ms内看到的是同一个画面。这种现象就是我们熟知的卡顿。
卡顿情况下的渲染VSYNC的由来:
在上面的知识基础上,我们知道了卡顿的产生原因,针对上面提到的渲染以及VSYNC信号,我们来详细说明一下其中的运作原理:
在讲解渲染的原理之前,我们需要先了解两个基本概念:
1. Refresh Rate:
概念: 代表了屏幕在一秒内刷新屏幕的次数
决定因素:这取决于硬件的固定参数,
举例: 60Hz
2. Frame Rate:
概念: 代表了GPU在一秒内绘制操作的帧数
决定因素:GPU
举例: 60fps
GPU负责计算和获取数据进行渲染,硬件方面负责将画面展示在频幕上面,两者完美的配合就可以达到最佳的情况,然而不幸的事,两者在实际情况下并不能总是保持步调一致,如果发生的帧率(fps)与刷新率(Hz)不一致的情况下。就会出现Tearing(图片撕裂)活着卡顿的情况。
A. 当GPU计算速度(fps)快于硬件刷新率(Hz)时:
Tearing(图片撕裂)的由来:
首先我们先介绍一下上面提到的Tearing,我们都知道,在绘制每一帧的时候,显示屏(硬件)会从图形芯片(内存)中取出GPU计算好的数据,并绘制该帧上的像素点,绘制方式为从上到下,一行一行的绘制。
理想情况下:期望显示屏在绘制完一帧之后,图形芯片整好能提供新帧的数据。
特殊情况下:当fps高于hz时,也就是GPU计算速度快于显示屏的绘制速度,这样就会导致在突破没有绘制完成时,就载入了新一帧的数据,由于存储帧数据的内存空间为同一块内存,新的数据就会将老的数据进行覆盖,这就导致最终绘制出来的帧是半个帧的新数据和半个帧的老数据。
发生Tearing现象的图片为了避免这种情况的发生,Android中引入了双缓冲机制和VSYNC(垂直同步)机制进行优化:
1. VSYNC(垂直同步)
垂直同步是为了保证当前帧可以完整显示而提出的机制,它会告知GPU在载入新帧之前,要等待屏幕绘制完成前一帧。这样就可以避免数据的覆盖,但是从机制中可以发现明显的漏洞,在等待硬件绘制完成的时间内,GPU是并不进行数据计算的,作为在资本主义社会中诞生的Android系统,这种现象是无法容忍的,为了保证“榨取每一分劳动力”的无上原则,引入了双缓冲机制。
在加入了垂直同步机制以后,系统由“固定16ms进行绘制”变成了“根据VSYNC信号”来进行绘制,也就是说,我们的优化目标变成了:保证VSYNC信号在16ms内发出。为了保证这个目标,Android系统默认的将帧率设定在60fps,也就是说Android系统每隔16ms就会发出VSYNC信号,来通知硬件进行刷新。
2. 双缓冲机制
正如其字面的理解,双缓冲就是采用两块内存区域作为存储。Android中所采用的双缓冲机制,意味着它可以在显示一帧的同时进行另一帧的处理。当显示缓冲A时,系统在缓冲B中构建新的帧。完成后,则交换缓冲。显示缓冲B,而A则被清空,继续下一帧的绘制(这里很像我们通常写代码时候用到的懒加载机制),保证了GPU的劳动力充分榨取。
但是这种双缓冲机制同样会有问题,当遇到上面我们提到的卡顿现象时,也就是某帧的绘制时间超过16毫秒时,双缓冲的问题就暴露出来了。如图所示,当缓冲B超时后,一个卡顿发生了。卡顿总是不好的,但更严重的问题在于图中代表CPU和GPU的图片之间的空白,这是对时间的浪费。在第一帧中,缓冲B超时,则在它被显示之前,都处于使用中的状态,而缓冲A因为为了填补空白,仍在显示,则也处于使用中的状态。我们知道,仅在收到垂直同步脉冲时,才会进行帧(缓冲)切换。则CPU和GPU受限于可以使用的缓冲数,不得不消极怠工(好像我们可以说:不是我不干活哦,是程序在编译哦)。
当然,针对这种现象,前辈们也提出了相应的解决方案,例如:三倍缓冲(也就是在A,B两块缓冲区域都处于使用状态时,创建C快缓冲区,这样就可以保证GPU在持续工作过程中)。
B. 当GPU计算速度(fps)慢于硬件刷新率(Hz)时:
通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。
fps小于Hz的情况在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同,也就是我们常说的“丢帧”。又或者另外一种情况,用户在使用过程中帧率从超过60fps突然掉到60fps以下,这样就会发生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。
导致卡顿的原因:
在了解绘制以及卡顿的原因以后,我们就可以结合一些性能优化工具来对我们的APP进行优化,从而使我们的APP具备润滑流畅的快感:
A. 性能监测工具
在优化性能之前,首要的是需要在�App中找到需要优化的点,进行逐一优化,各个击破,这时就需要用到一些的性能监测工具,从哪里找这些工具呢?�谷歌为我们这群懒人们提供了一个高效且�易用的可视化工具:Profile GPU Render。
位置:
开发者选项--->�Profile GPU Render--->On Screen as Bars
�界面介绍:
这样就可以在屏幕下面看到如心电图一样的动态性能监视图,当在使用APP的过程中,下部的监视图会从左至右进行绘制。整个屏幕可以分为三个部分,头顶部的心电图代表的是,通知栏的绘制性能情况(一般不咋用),下部是我们着重需要重点关注的,也就是当前激活的Application(我们需要优化的app)的性能心电图。
性能监视图在下部的监视图中,除了那根明显的绿色线条用于标注16ms以外(每个程序员在优化的过程中需要保证的就是将在整个APP运行过程中,整个心电图中的每一根线都在这条绿线以下),其下部的心电图根据颜色分为三个部分:
A.1 蓝色代表队:在该帧的计算过程中CPU进行转换及缓存(创建和展示displaylist中的内容)所消耗掉时间。而绘制时间则是由转换成GPU可以识别的格式所消耗掉时间(由drawable图片通过命令或者通过canvas绘制两种方式)以及将格式缓冲进Displaylist所消耗掉时间所构成。
如果这个部分的消耗很大的话,说明当前帧中可能有很多的用到图片资源的控件需要绘制,活着说当前页面中有一个自定义控件非常复杂,需要消耗很多的逻辑计算时间。
格式转换及缓存两部分A.2 红色代表队:在该帧的计算过程中将结果存储进DisplayList的集合中,然后利用该集合汇总的信息,转换成图像,也就是绘制图像执行所消耗掉时间。通常Android中的会通过Open GL ES API来将集合中的信息传入到GPU当中,随后通过GPU将信息转化成像素点,绘制在屏幕上。
也就是说,如果我们需要绘制的信息越多的情况下,比如说华丽复杂的自定义时,就会消耗很多时间,同时还有重要的一点就是:如果图片中出现重复绘制的情况,也会相应增加GPU的无谓消耗时间。这部分的内容也会增加红色代表队的时间消耗。
A.3 橙色代表队:在该帧的计算过程中所CPU在完成逻辑计算后,但需要等待GPU绘制完成,才能进行下一步工作,所消耗掉的时间。
这里如果出现增长的话,就是意味着你的GPU上承载着高负荷的工作,也就是说你当前有非常多的Open GL ES API命令需要执行。
补充:
在上面提到的诸如Open GL ES API,CPU进行转换及缓存等一些名词是不是很懵逼?(别装,肯定的),在这里我继续补充一下从谷歌提供的视频中针对这些步骤的详细解释:
1. XML布局中的文弱少女是如何转换成的频幕上的妖艳贱货的:
首先需要通过resterization(栅格化)将图片,图形或者文字一类转化成频幕可以展示的像素的过程。 当然,这个过程绝逼是非常消耗时间的, 这里的GPU就是将这个漫长的过程变得飞快。
由于GPU对栅格化的基本格式有特定的要求,主要格式为:polygons,textures和images。CPU在这里负责将图片,文字首先转化成成 polygons,textures和images格式,然后传递给GPU进行栅格化处理。
例如一个button需要先在CPU中进行格式转换(polygons等),然后传递给GPU进行栅格化2. 看似平整的地面,总有暗坑等你踩
但是也许你可以发现这里有一个坑比的地方,button转换成特定的polygons是一个时间消耗过程,再由polygons传递给GPU又是一个时间消耗过程,而由CPU传递给GPU同样是一个非常耗时的过程。这样就意味着,GPU中进行栅格化所节省下来的时间,可能在这里被消耗大半。
幸运的是Open GL考虑到了这一点,它提供了一个类似缓存到机制:CPU上传到GPU中的资源,可以作为缓冲保存在GPU当中,在下次再次利用的过程中,就省去了CPU的格式转换和CPU上传到GPU的过程消耗。Android系统就灵活利用到了这一点,它在系统启动过程中,就将主题中的系统资源以一个单一polygons的形式上传至GPU,以后在调用系统资源时,就可以直接在GPU中取到相应资源,而不需要转换和传递。这就是加载Android系统图片为啥这么快的原因。
然而,有了这个机制就可以万事大吉了?并不,随着UI画的图越来越诡异,产品设计的动画越来越彪悍,GPU的缓冲机制变得几乎形同虚设,因为每一个图片都是不同的,都无法服用,因此GPU中的缓存资源只能通过不断被覆盖来达到相应效果,谢天谢地,Android系统提供了差异化绘制机制,简单来说就是缓存的旧资源与即将写入的新资源进行对比,只对发生了改变的部分进行重新处理。以此缓解GPU的压力。
3. 妈妈说,破掉的东东要扔掉
在上面,我们有提到,有DisplayList对CPU处理好的格式资源以及需要进行的相应的绘制指令,进行接收。这里的DispalyList在特殊情况下,可以对其接收的信息进行复用,举例来说:
如果一个button改变了其位置:GPU可以将DisplayList中的信息可以进行复用。
如果一个button改变了其大小或者其形状,表面颜色发生改变(视觉上的形体,色彩改变 ):GPU就无法使用之前CPU传递来的DisplayList,需要通过CPU进行重新格式转换,然后将命令和转换好的资源存入一个新的DisplayList当中。
4. Android系统都是强迫症!
我们需要注意的是,我们的每一个小小的改变,都会引起整个view的大量工作,举例来说:
4.a 改变界面中一个view或viewgroup中的子view的尺寸大小,原先的measure流程测过的所有尺寸就无效了,系统会遍历整个view hierarchy,对每个view进行重新measure。
4.b 改变界面中一个view或者一个viewgroup中的子view的位置,原先界面中的所有view位置信息就全部无效了,系统会通过requestLayout来重新确定每个view的位置。
这些个步骤都是非常耗时的,当界面中有庞大且复杂的view出现时,就会严重影响程序的性能。这里我们可以通过将整个view hierarchy扁平化处理,这样可以有效的降低改动部分view,所造成的整个view hierarchy重新测量或重新定位所消耗掉时间。
Hierarchy Viewer视图B. Overdraw
在上面的介绍中,我们得知,在CPU通过Open GL将转换后的资源交给GPU进行绘制时,如果出现大量重复绘制的情况,会加重GPU的负担,那么降低重复绘制的内容,就是我们需要关注的其中一个重点。那么,问题来了,重复绘制查找哪家强?Android系统中给我们同样提供了一个用于监测重复绘制的工具---Overdraw。
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。
幸运的是,谷歌为我们考虑到了这一点,在系统中的手机设置里面的开发者选项,步骤为:"系统设置"-->"开发者选项"-->"调试GPU过度绘制"打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。在勾选该选项后,频幕中会出现四种色调:
Overdraw四种绘制情况其中,四种颜色:蓝色,绿色,粉色,红色分别代表:1次绘制,2次绘制,3次绘制,四次绘制。还有一种是原色,代表的是无重复绘制。
多次绘制的原因有很多,其中有可能是因为你UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景活着绘制了不可见的UI元素。这里需要提醒的是,通常情况下,我们创建的Activity在默认情况下,theme会给window设置一个纯色的背景. 因为我们这里不想使用这个默认的背景,故而给layout加了一层背景, 导致了多重绘制背景.可以通过:
getWindow().setBackgroundDrawable(null);
将这一层默认添加的背景消除。
到此为止,是不是发现,随着科技和优化机制的革新,更漂亮,更华丽的效果也得以实现,两者相互推进,使得我们的应用越来越完美,这就是我们为何需要优化我们APP的根本原因。本篇文章只是向程序优化的第一步,随后,我会继续学习视频和相关文档,推出新的文章,与大家一起分享和讨论。