美文网首页性能优化—Android安卓面试
Android性能调优(2) — 布局层级与Overdraw

Android性能调优(2) — 布局层级与Overdraw

作者: godliness | 来源:发表于2018-05-04 12:31 被阅读48次

    一、前言

    通过《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文件中我们最常用到的有:LinearLayoutFrameLayoutRelativeLayout但是这三种布局的性能孰优孰劣?不同的业务场景该如何选择呢?我们通过使用相关工具以及结合源码进行相关分析:

    我们的实验:外部一个布局容器,子View由11个TextView组成,分别观察他们的MeauseLayoutDraw所消耗的时间:

    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、利用工具对布局进行分析,去除冗余布局,以及不合理的布局。

    性能优化其实不仅仅是一种技术,而是一种思想,你只听过它的高大上,却不知道它是各个细节处的深入研究和处理。

    相关文章

      网友评论

        本文标题:Android性能调优(2) — 布局层级与Overdraw

        本文链接:https://www.haomeiwen.com/subject/wjrdrftx.html