一、前言
通过《Android性能调优(1) — 启动时间》对App启动速度优化有了一个新的认识,提高应用启动速度的同时也带来更好的用户体验效果。该篇文章来探讨下App启动之后影响界面加载性能的另一个问题 — 布局层级与Overdraw。
二、CPU与GPU工作流程
首先在讨论该问题之前,让我们先来看下计算机是如何将视图呈现在我们眼前的:
1、GPU的由来
CPU的任务繁多,做逻辑计算外,还要做内存管理、显示操作,因此在实际运算的时候性能会大打折扣,在没有GPU的时代,不能显示复杂的图形,其运算速度远跟不上今天复杂的三维游戏的要求。即使CPU的工作频率很高,它对绘制图形的提高也不大,这是GPU的设计就出来了。


黄色的Control为控制器,用于协调控制整个CPU的运行,包括取出指令、控制其他模块的运行等;绿色的ALU(Arithmetic Logic Unit)是算数逻辑单元,用于进行数学、逻辑运算;
橙色的Cache(CPU高速缓存)和DRAM分别为缓存和RAM(随机存储器),用于存储信息。
从结构图可以看出,CPU的控制器较为复杂,而ALU数量较少。因此CPU擅长各种复杂的逻辑运算,单不擅长数学尤其是浮点运算。

2、60HZ刷新频率的由来
12fps:由于人类眼睛特殊生理结构,如果所看画面之帧率高于每秒钟约10-12帧的时候,就会认为是连贯的
24fps:有声电影的拍摄以及播放帧率均为每秒钟24帧,对一般人而言已算是可接受
30fps:早期的高动态电子游戏,帧率少于每秒20帧率就会显得不连贯,这是因为没有动态模糊使流程度降低。
60fps:在与手机交互过程中,如触摸和反馈60帧以下人是能够感觉出来的,60帧以上不能察觉变化
当帧率低于60fps时感觉画面有卡顿和迟滞现象
Adroid系统每隔16ms发出VSYNC信号(1000ms/60=16.66ms),触发对UI进行渲染,如果每次渲染都成功这样就能够达到流程的画面所需要的60fps,为了能够实现60fps,这意味着计算渲染的大多数操作都必须在16ms内完成。
3、DisplayList
荐:Android Vsync原理
CPU、GPU、显示器三个部分:
CPU负责计算数据,把计算好数据交给GPU 负责Measure,Layout,Record,Execute等计算操作。
GPU会对图形数据进行渲染, 负责Rasterization(栅格化)操作,然后显示器负责把Buffer里的数据呈现到屏幕上。

以时间的顺序来看下将会发生的异常:
Step1: Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,而且赶在Display显示下一帧前完成
Step2.:因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,正常显示第1帧
Step3: 由于某些原因,比如CPU资源被占用,系统没有及时地开始处理第2帧,直到第2个VSync快来前才开始处理
Step4:第2个VSync来时,由于第2帧数据还没有准备就绪,显示的还是第1帧。这种情况被Android开发组命名为“Jank”。
Step5.:当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync。所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因大家应该都看到了,就是CPU没有及时地开始着手处理第2帧的渲染工作,以致“延误军机”。
将我们写的UI控件如Button转换成特定的向量图形是一个时间消耗过程,再由向量图形传递给GPU又是一个时间消耗过程,而由CPU传递给GPU同样是一个非常耗时的过程。这样就意味着,GPU中进行栅格化所节省下来的时间,可能在这里被消耗大半。
幸运的是Open GL考虑到了这一点,它提供了一个类似缓存到机制:CPU上传到GPU中的资源,可以作为缓冲保存在GPU当中,在下次再次利用的过程中,就省去了CPU的格式转换和CPU上传到GPU的过程消耗。
Android系统就灵活利用到了这一点,它在系统启动过程中,就将主题中的系统资源以一个单一向量图形的形式上传至GPU,以后在调用系统资源时,就可以直接在GPU中取到相应资源,而不需要转换和传递。这就是加载Android系统图片为啥这么快的原因。
然而,有了这个机制就可以万事大吉了?并不,随着UI画的图越来越诡异,产品设计的动画越来越彪悍,GPU的缓冲机制变得几乎形同虚设,因为每一个图片都是不同的,都无法复用,因此GPU中的缓存资源只能通过不断被覆盖来达到相应效果,Android系统提供了差异化绘制机制,简单来说就是缓存的旧资源与即将写入的新资源进行对比,只对发生了改变的部分进行重新处理。以此缓解GPU的压力。
在上面,我们有提到,有DisplayList对CPU处理好的格式资源以及需要进行的相应的绘制指令,进行接收。这里的DispalyList在特殊情况下,可以对其接收的信息进行复用,举例来说:
1)如果一个Button改变了其位置:GPU可以将DisplayList中的信息可以进行复用。
2)如果一个Button改变了其大小或者其形状,表面颜色发生改变(视觉上的形体,色彩改变 ),GPU就无法使用之前CPU传递来的DisplayList,需要通过CPU进行重新格式转换,然后将命令和转换好的资源存入一个新的DisplayList当中。
三、卡顿原因分析
1、16ms
当一帧画面的渲染时间超过16ms的时候,垂直同步机制会让显示器(硬件)等待GPU完成栅格化渲染操作,这样会让一帧画面多停留在16ms甚至更多,这就是卡顿的由来。

从上图可以看出:CPU和GPU的处理时间因为各种原因都大于一个VSync的间隔(16.6ms),所以在第二个VSync还在处理1区域的绘制时, 试想用户盯着同一张图看了32ms而不是16ms,当然很容易察觉出卡顿感,哪怕仅仅出现一次掉帧,用户都会发现动画不是很顺畅,大家在察觉到APP卡顿的时候
16ms的时间主要被两件事情占用:
第一件:将UI转换为一些列的多边形和纹理
第二件:CPU传递处理数据到GPU。
所以很明显,我们要缩短这两部的时间,也就是需要尽量减少对象转换次数,以及上传数据的次数。
2、如何避免
第一件:CPU减少xml转换成对象的时间 ,布局层级越深,展开所花费的时间越长,控件的个数与时间也是成正比。
第二件:GPU减少重复绘制的时间,避免Overdraw。

四、实战解决过渡渲染(Overdraw)
1、什么是过渡渲染
GPU的绘制过程就像是刷墙一样,一层层的进行,16ms刷新一次,这样就会造成图层覆盖的现象,既无用的图层也被绘制在底层,造成不必要的浪费。
理论上一个像素点只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,Overdraw由此产生。
2、查看过渡渲染
通过系统自带的调试工具来查看Overdraw:设置--开发者选项--调试GPU过渡绘制:

选择第二项,第三项是针对色盲。

其中颜色代表图层渲染层级:分别代表一层、二层、三层、四层。
过渡绘制颜色等级分为5个等级:
1、原色 未发生过渡绘制
2、蓝色 1次过渡绘制,这部分像素点只在屏幕上绘制了两次
3、绿色 2次过渡绘制,这部分像素点在屏幕上绘制了三次
4、浅红色 3次过度绘制,这部分像素点在屏幕上绘制了四次
5、红色 4次过渡绘制 这部分像素点在屏幕上绘制了五次,再次超过部分也是按照红色显示。
在实际开发中,尽可能保证更多的是蓝色,极少部分的绿色或者浅红色,最好不要出现红色
项目实际截图:

3、避免过渡渲染
过渡绘制的几种常见情况:
1)自定义控件中onDraw()方法中做了较多的重复绘制
2)布局层级太深,层叠性太强,用户看不到的区域也会进行渲染导致耗时增加
3)一个容易忽略的点是:我们Activity使用的Theme可能会默认的加上背景色,不需要的情况下可以去掉:
getWindow().setBackgroundDrawable(null);
Android系统为我们做了哪些优化:
1)CPU转移到GPU是一件麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面。
2)在下次需要渲染的时候直接进行操作,所以如果你更新了GPU所Hold住的纹理内容,那么值钱保存的状态就丢失了。
3)在Android里面那些有主题所提供的资源,例如Bitmap是,Drawables都是一起打包到统一的Texture纹理当中,然后在传递到GPU里面,这意味着每次需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的,当然随着UI组件的越来越丰富,有了更多演变的形态,例如显示图片的时候,需要先经过CPU的计算加载的内容中,然后传递给GPU进行渲染。文字的显示比较复杂,需要先经过CPU换算成纹理,然后交给GPU进行渲染,返回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容,动画则存在一个更加复杂的操作流程。
4)为了能够使得APP流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等操作。

5)布局优化去掉背景

6)去掉二级容器背景

五、布局容器选择
在布局xml文件中我们最常用到的有:LinearLayout、FrameLayout、RelativeLayout但是这三种布局的性能孰优孰劣?不同的业务场景该如何选择呢?我们通过使用相关工具以及结合源码进行相关分析:
我们的实验:外部一个布局容器,子View由11个TextView组成,分别观察他们的Meause、Layout、Draw所消耗的时间:
1、LinearLayout
1)当LinearLayout未使用layout_weight属性:
Measure:0.039ms Layout:0.018ms Draw:6.267ms

2)当LinearLayout使用了layout_weight属性:
Measure:0.107ms Layout:0.069ms Draw:6.715ms

3)可以很明显看到LinearLayout的layout_weight属性对性能有影响,这是为什么?

可以看到LinearLayout的onMeause()方法首先判断方向(水平或者垂直),然后对单一方向做measure操作。
那使用layout_weight属性会怎样呢?
以measureVertical()为例:


用于标记当前设置权重的总大小。
紧接着在方法的最后如果totalWeight大于0(也就是我们在布局中使用了layout_weight属性并设置了非零),此时会进行第二遍measure操作,否则只会measure一遍。

2、FrameLayout
Measure:0.134ms Layout:0.050ms Draw:7.034ms

由于FrameLayout的onMeasure()方法篇幅过长,故不贴出代码了。查看FrameLayout的onMeasure代码发现它也是measure两次。第一次:测量控件之间的Margin,第二次测量控件自身大小。
3、RelativeLayout
Measure:0.291ms Layout:0.039ms Draw:6.536ms

根据源码发现RelativeLayout会对子View做两次measure。这是为什么?


首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A,B 2个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向分别进行一次排序测量。
4、三种常用布局对比:
LinearLayout:Measure:0.039ms Layout:0.018ms Draw:6.267ms
LinearLayout:Measure:0.107ms Layout:0.069ms Draw:6.715ms (weight)
FrameLayout:Measure:0.134ms Layout:0.050ms Draw:7.034ms
RelativeLayout:Measure:0.291ms Layout:0.039ms Draw:6.536ms
考虑误差问题实际三者的Layout与Draw相差不大,差距较大的为Measure过程。
5、总结:
1)RelativeLayout会measure两次子View,不使用layout_weight属性的LinearLayout只会measure一次,而设置了layout_weight属性会measure两次。
2)RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
3)在不影响层级深度情况下,优先使用LinearLayout和FrameLayout而不是RelativeLayout。
4)能用两层LinearLayout(LinearLayout嵌套,展开时会呈现递增式耗时),尽量使用一个RelativeLayout,在时间上此时RelativeLayout耗时更小。另外LinearLayout慎用layout_weight属性,它将会增加measure耗时(首先按照控件本身大小进行分配,然后将剩余的空间按照weight在分配给每个控件)。由于使用LinearLayout的layout_weight,大多数时间是不一样的,这会降低测量的速度。这只是一个如何合理使用LinearLayout的案例,必要的时候,你要小心考虑是否使用layout_weight。
5)减少布局层级深度才是王道,让onMeasure做延迟加载,用viewStub,include等一些技巧。
6)merge标签减少布局层级。
六、Hierarchy View
这是一款帮助我们解决布局问题的神器,有关于它的使用资料实在是太多了,不再过多叙述。
七、总结
1、项目开发过程中尽可能避免Overdraw。
2、尽可能减少布局层级。
3、利用工具对布局进行分析,去除冗余布局,以及不合理的布局。
性能优化其实不仅仅是一种技术,而是一种思想,你只听过它的高大上,却不知道它是各个细节处的深入研究和处理。
网友评论