在开发过程中,往往会听到 “性能优化” 这个概念,这个概念很大,比如网络性能优化、耗电量优化等等,对 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、查看页面是否存在过度绘制
如下图所示:
图1:开发者选项
图2:开启GPU过度绘制
开启GPU过度绘制后,点击应用,可以看到各种颜色的区域。依据过度绘制的层度可以分成:
接下来我们从设计的角度来看下App是否GPU绘制过度,看一下以下几个界面:
图4:QQ浏览器
图6:Google浏览器
从上图我们可以看出,QQ浏览器页面GPU绘制比较正常基本都是在1x-2x范围内,滴滴顺风车(优化前)和Google浏览器过度绘制较为严重,基本都是3x-4x。
颜色越深代表绘制的次数越大,当一个屏幕大部分都被粉丝或红色占据时,我们就必须考虑优化了。
3.2、View的绘制流程
为了更好地理解 View 性能优化的原理,以及造成 “卡顿” 的可能原因,我们简单讲解下View的绘制流程,为后续的 hierarchyviewer 分析做铺垫。
我们都知道,View的绘制分为三个阶段:测量、布局和绘制,这三个阶段各自的作用如下:
当一个 Activity 对象被创建完成之后,会将一个 DecorView 对象添加到 Window 中,同时会创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 对象建立联系,然后绘制流程就会从 ViewGroup 的 performTraversals()
方法开始执行,如下图所示:
图7:view的绘制流程
整个绘制流程从 ViewRootImpl 的 performTraversals()
方法开始,在该方法内会调用 performMeasure()
方法进行测量子 View(也就是根 View,顶级的 ViewGroup),调用完 performMeasure()
后,会接着调用 performLayout()
和 performDraw()
进行 View 的布局和绘制。
4.1、较多的背景重叠
过度绘制是指屏幕中某个范围的像素在单个帧中被多次渲染(超过一次),比如父控件设置了背景色,子控件设置了图片显示或者文本显示,这样在子控件的对应区域,就会渲染两次。每次渲染都会带来性能消耗,同一区域渲染的次数越多,那么带来的消耗就越大。
举例来说,粉刷一个房间或一间房子时,给墙壁涂上颜色需要做大量的工作。假如还要重新粉刷一次的话,第二次粉刷的颜色会覆盖住第一次的颜色,第一次的颜色就永远不可见了,等于第一次粉刷做的大量工作就完全被浪费掉。
以"乘客端-顺风车-车服务"页面为例,绘制颜色呈现为红色的原因在于其页面渲染共经历5层绘制:
图10:车服务Loyout布局
分析布局可知:多层布局重复设置了背景色导致Overdraw。
4.2、复杂的Layout层级
Android的布局文件的加载是LayoutInflater利用pull解析方式来解析,然后根据节点名通过反射的方式创建出View对象实例;同时嵌套子View的位置受父View的影响,类如RelativeLayout、LinearLayout等经常需要measure两次才能完成,而嵌套、相互嵌套、深层嵌套等的发生会使measure次数呈指数级增长,所费时间呈线性增长。
图12:初始状态View个数及耗时(优化前)
使用Hierarchy Viewer来看查看一下设置界面,可以从下图中得到首页界面的一些数据及存在的问题:
由此得到结论:Android渲染需要消耗时间,布局越复杂,性能就越差。那么随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差。
优化的目的,主要就是减少绘制时间。
5.1、过度渲染解决
去掉冗余background后的Overdraw如下图所示:
图13:顺风车乘客端(优化后)
另外一个容易忽略的点是我们的Activity使用的Theme可能会默认的加上背景色,不需要的情况下也可以去掉。
5.2、Layout层级优化
布局的优化其实说白了就是减少层级,越简单越好,减少overdraw,就能更好的突出性能。
5.2.1、尽量使用相对布局
一般情况下用LinearLayout的时候总会比RelativeLayout多一个View的层级。而每次往应用里面增加一个View,或者增加一个布局管理器的时候,都会增加运行时对系统的消耗,因此这样就会导致界面初始化、布局、绘制的过程变慢。
选择布局容器的基本准则:
5.2.2、使用标签重用Layout
如果一些布局在许多布局文件中都需要被使用,我们就可以把它单独写在一个布局中,然后使用这个标签在需要使用它的地方把这个布局加进去,这样就达到了重用的目的,最典型的一个用法就是,如果我们自定义了一个TitleBar,这个TitleBar可能需要在每个Activity的布局文件中都使用到,这样我们就可以使用这个标签来实现。
图16:include标签
直接使用include标签的layout来指定就可以把这个bts_home_tip_full_layout的布局文件加入进去,这样在每个Activity中我们就可以使用include标签来重用这个布局了,不需要在每个里面都重复写一个bts_home_tip_full_layout的布局了,下面我们来看看这个bts_home_tip_full_layout的布局文件。总结一点:这个标签主要是做到布局的重用,使用这个标签可以把公共布局嵌入到所需要嵌入的地方。
在使用了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来检查和优化布局。