顺风车Android性能优化之View布局优化

一、问题背景

在开发过程中,往往会听到 “性能优化” 这个概念,这个概念很大,比如网络性能优化、耗电量优化等等,对 RD 而言,最容易做的或者是影响最大的,应该是 View 的性能优化。当业务愈加庞大、界面愈加复杂的时候,没有一个良好的开发习惯和 View 布局优化常识,做出来的界面很容易出现 “卡顿” 现象,从而严重影响用户体验。

结合具体业务特点进行梳理,对于性能问题的产生大致概括为以下3个方面:

1、首先,需求开发或重构过程中,由于 RD 同学的关注点主要放在单个业务开发上,所以很容易忽略性能上的问题,即使在开发过程中发现了卡顿问题,但由于业务紧张,也不太会放下当前的工作去处理性能方面的问题。

2、其次,测试过程中时而会有 QA 同学反馈说某些页面相比之前的版本出现了严重的卡顿问题,但有时通过发版平台、logcat 等并未找到该机型的卡顿记录,最终一些可能存在的性能问题也就不了了之。

3、再次,大部分PM同学关注的都是 RD 是否有100%实现需求,UI/UE 同学关注的是应用的交互体验是否良好,并没有太多同学会去关注应用的性能问题,对于性能较好的手机可能体验不到差距,对于中低档手机,流畅度却起着关键的作用。

二、问题归类

引起卡顿的原因从细节上可分为以下几类原因:

外部因素最为致命!日常开发中更多的应该关心布局的嵌套层级和冗余资源。

比如,当需要将一个 TextView 和一张图片放在一起展示时,我们可以考虑使用 TextView 的 drawableLeft(drawableRight、drawableTop、drawableBottom) 属性来设置图片,而不是使用一个 LinearLayout 来将 TextView 和 ImageView 封装在一起,这样就能减少 View 的绘制层级。

又比如,子元素和父元素都是相同的背景时,就不必在每个子元素中都添加背景属性,等等。

接下来,我们针对过度绘制和布局层级进行测试分析。

三、问题复现

3.1、查看页面是否存在过度绘制

  • 方法一:可通过打开手机“开发者选项”->"调试GPU过度绘制开关",就可以在手机屏幕上查看绘制情况。
  • 方法二:通过 adb 命令开启 GPU 过度绘制调试 :adb shell setprop debug.hwui.overdraw show

如下图所示:

       图1:开发者选项

    图2:开启GPU过度绘制                 

开启GPU过度绘制后,点击应用,可以看到各种颜色的区域。依据过度绘制的层度可以分成: 

  • 原色:无过度绘制 (一个像素只被绘制了一次)  
  • 蓝色:1 次过度绘制 (一个像素被绘制了两次)  
  • 绿色:2 次过度绘制 (一个像素被绘制了三次) 
  • 粉色:3 次过度绘制 (一个像素被绘制了四次)  
  • 红色:4 次及以上过度绘制(一个像素被绘制了五次以上)


                                           图3:GPU绘制图解

接下来我们从设计的角度来看下App是否GPU绘制过度,看一下以下几个界面:

               图4:QQ浏览器

     图5:顺风车-车服务(优化前) 

           图6:Google浏览器

从上图我们可以看出,QQ浏览器页面GPU绘制比较正常基本都是在1x-2x范围内,滴滴顺风车(优化前)和Google浏览器过度绘制较为严重,基本都是3x-4x。

颜色越深代表绘制的次数越大,当一个屏幕大部分都被粉丝或红色占据时,我们就必须考虑优化了。

 

3.2、View的绘制流程

为了更好地理解 View 性能优化的原理,以及造成 “卡顿” 的可能原因,我们简单讲解下View的绘制流程,为后续的 hierarchyviewer 分析做铺垫。

我们都知道,View的绘制分为三个阶段:测量、布局和绘制,这三个阶段各自的作用如下: 

  • measure: 为整个 View 树计算实际的大小,即设置实际的高(对应属性:mMeasureHeight)和宽(对应属性:mMeasureWidth),每个 View 的控件的实际宽高都是由父视图和本身视图所决定的。
  • layout:为将整个根据子视图的大小以及布局参数将 View 树放到合适的位置上。
  • draw:利用前两部得到的参数,将视图显示在屏幕上。

当一个 Activity 对象被创建完成之后,会将一个 DecorView 对象添加到 Window 中,同时会创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 对象建立联系,然后绘制流程就会从 ViewGroup 的 performTraversals() 方法开始执行,如下图所示:

 

                      图7:view的绘制流程

整个绘制流程从 ViewRootImpl 的 performTraversals() 方法开始,在该方法内会调用 performMeasure() 方法进行测量子 View(也就是根 View,顶级的 ViewGroup),调用完 performMeasure()后,会接着调用 performLayout() 和 performDraw() 进行 View 的布局和绘制。

四、问题分析

4.1、较多的背景重叠

过度绘制是指屏幕中某个范围的像素在单个帧中被多次渲染(超过一次),比如父控件设置了背景色,子控件设置了图片显示或者文本显示,这样在子控件的对应区域,就会渲染两次。每次渲染都会带来性能消耗,同一区域渲染的次数越多,那么带来的消耗就越大。

举例来说,粉刷一个房间或一间房子时,给墙壁涂上颜色需要做大量的工作。假如还要重新粉刷一次的话,第二次粉刷的颜色会覆盖住第一次的颜色,第一次的颜色就永远不可见了,等于第一次粉刷做的大量工作就完全被浪费掉。

以"乘客端-顺风车-车服务"页面为例,绘制颜色呈现为红色的原因在于其页面渲染共经历5层绘制:

  • 第一层为地图页,是平台规定的,而顺风车页面对其进行了遮盖处理,所以即使不可见也不可去掉。可参考下图的“快车”地图页
                 图8:地图页面
  • 第二层为车服务页面的背景渲染,顺风车统一背景色,所以不可修改为无色背景
                    图9:车服务页面
  • 第三层为页面布局中的卡片布局背景
  • 第四层为页面具体控件元素的渲染
  • 第五层为RD在layout中多添加的一层布局背景
 
Layout如下图所示

                                       图10:车服务Loyout布局

分析布局可知:多层布局重复设置了背景色导致Overdraw。

4.2、复杂的Layout层级

 Android的布局文件的加载是LayoutInflater利用pull解析方式来解析,然后根据节点名通过反射的方式创建出View对象实例;同时嵌套子View的位置受父View的影响,类如RelativeLayout、LinearLayout等经常需要measure两次才能完成,而嵌套、相互嵌套、深层嵌套等的发生会使measure次数呈指数级增长,所费时间呈线性增长。

                               图11:顺风车车服务首页初始状态(优化前)
 

            图12:初始状态View个数及耗时(优化前)

使用Hierarchy Viewer来看查看一下设置界面,可以从下图中得到首页界面的一些数据及存在的问题:

  • 嵌套共计7层(仅setContentView设置的布局),布局嵌套过深;
  • 共绘制332个View,以及若干个无用布局。
  • 页面渲染总耗时为:50.574ms

由此得到结论:Android渲染需要消耗时间,布局越复杂,性能就越差。那么随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差。

五、解决问题

优化的目的,主要就是减少绘制时间。

5.1、过度渲染解决

去掉冗余background后的Overdraw如下图所示:

 

       图13:顺风车乘客端(优化后)      

另外一个容易忽略的点是我们的Activity使用的Theme可能会默认的加上背景色,不需要的情况下也可以去掉。

5.2、Layout层级优化

布局的优化其实说白了就是减少层级,越简单越好,减少overdraw,就能更好的突出性能。

5.2.1、尽量使用相对布局

 一般情况下用LinearLayout的时候总会比RelativeLayout多一个View的层级。而每次往应用里面增加一个View,或者增加一个布局管理器的时候,都会增加运行时对系统的消耗,因此这样就会导致界面初始化、布局、绘制的过程变慢。

                                图14:布局比较

选择布局容器的基本准则:

  • 在RelativeLayout和LinearLayout同时能够满足需求时,尽可能的使用 RelativeLayout 以减少 View 层级,因为可以通过扁平的RelativeLayout降低LinearLayout嵌套所产生布局树的层级,使 View 树趋于扁平化。
  • 在不影响层级深度的情况下,使用 LinearLayout 和 FrameLayout 而不是 RelativeLayout。

                                      图15:RelativeLayout布局

5.2.2、使用标签重用Layout

  • 布局重用之include

如果一些布局在许多布局文件中都需要被使用,我们就可以把它单独写在一个布局中,然后使用这个标签在需要使用它的地方把这个布局加进去,这样就达到了重用的目的,最典型的一个用法就是,如果我们自定义了一个TitleBar,这个TitleBar可能需要在每个Activity的布局文件中都使用到,这样我们就可以使用这个标签来实现。

                                   图16:include标签

直接使用include标签的layout来指定就可以把这个bts_home_tip_full_layout的布局文件加入进去,这样在每个Activity中我们就可以使用include标签来重用这个布局了,不需要在每个里面都重复写一个bts_home_tip_full_layout的布局了,下面我们来看看这个bts_home_tip_full_layout的布局文件。

总结一点:这个标签主要是做到布局的重用,使用这个标签可以把公共布局嵌入到所需要嵌入的地方。

  • 布局重用之merge

在使用了include后可能导致布局嵌套过多,多余不必要的layout节点,从而导致解析变慢。例如Layout A中的RelativeLayout布局中使用了include标签,而在引入的布局文件中也包含了RelativeLayout,那么在Layout A的布局中实际会有两个RelativeLayout被加载进行渲染。

在layout中可以使用merge标签来作为include标签的一种辅助扩展来使用,其主要作用是为了防止在引用布局文件时产生多余的布局嵌套,减少布局的深度。

不必要的节点和嵌套可通过hierarchy viewer或设置->开发者选项->显示布局边界查看。

                                         图17:merge标签

5.2.3、按需载入视图

viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,既不会占用显示,也不会占用位置,从而在解析layout时节省cpu和内存。 使用ViewStub并不会影响UI初始化时的性能。
viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。

ViewStub使用延迟加载的方式,当需要时才会加载,避免资源的浪费,减少渲染时间,在需要的时候才加载View。

                            图18:viewstub标签

最开始使用setContentView(R.layout.bts_home_entrance_layout)的时候,ViewStub只是起到一个占位符的作用,它并不会占用空间,所以对其他的布局没有影响。

当我们点击Button的时候,我们就可以把ViewStub的layout属性指定的布局加载进来,用它来替换ViewStub,这样就把我们需要加载的内容加载进来了。

这里的ViewStub控件的layout指定为bts_home_not_open_layout。当点击button隐藏的时候不会显示bts_home_not_open_layout,而点击button显示的时候就会用bts_home_not_open_layout替代ViewStub。

 

现在我们再使用Hierarchy Viewer来检测一下:

                            图19:优化后的车服务首页布局层次

          图20:优化之后的View个数及耗时

优化后:
1. 控件数量从332个减少到227个,减少31.6%;

2.优化后的页面总耗时为39.463ms,页面优化提高21.9%

六、建议

1.保持整体背景统一

建议前期在设计时尽量保持整体背景统一,另外可以检查在布局和代码中设置的背景,有些背景是被隐藏在底下的,它永远不可能显示出来,这种没必要的背景尽量要移除,因为它很可能会严重影响到app的性能。

2.检查和优化布局

首先推荐用Android提供的布局工具Hierarchy Viewer来检查和优化布局。

  • 建议1:如果嵌套的线性布局加深了布局层次,可以使用相对布局来取代。
  • 建议2:用标签来合并布局,这可以减少布局层次。
  • 建议3:用标签来重用布局,抽取通用的布局可以让布局的逻辑更清晰明了。

你可能感兴趣的:(顺风车Android性能优化之View布局优化)