通过《Android性能优化(一)之启动加速35%》我们获得了闪电般的App启动速度,那么在应用启动完毕之后,UI布局也会对App的性能产生比较大的影响,如果布局写得糟糕,显而易见App的表现不可能流畅。
那么本文我同样基于实际案例,针对应用的布局进行优化进而提升App性能。
根据Google官方出品的Android性能优化典范,60帧每秒是目前最合适的图像显示速度,事实上绝大多数的Android设备也是按照每秒60帧来刷新的。为了让屏幕的刷新帧率达到60fps,我们需要确保在时间16ms(1000/60Hz)内完成单次刷新的操作(包括measure、layout以及draw),这也是Android系统每隔16ms就会发出一次VSYNC信号触发对UI进行渲染的原因。
如果整个过程在16ms内顺利完成则可以展示出流畅的画面;然而由于任何原因导致接收到VSYNC信号的时候无法完成本次刷新操作,就会产生掉帧的现象,刷新帧率自然也就跟着下降(假定刷新帧率由正常的60fps降到30fps,用户就会明显感知到卡顿)。
理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,Overdraw由此产生。
我们可以通过调试工具来检测Overdraw:设置——开发者选项——调试GPU过度绘制——显示过度绘制区域。
原色 – 没有过度绘制 – 这部分的像素点只在屏幕上绘制了一次。 蓝色 – 1次过度绘制– 这部分的像素点只在屏幕上绘制了两次。 绿色 – 2次过度绘制 – 这部分的像素点只在屏幕上绘制了三次。 粉色 – 3次过度绘制 – 这部分的像素点只在屏幕上绘制了四次。 红色 – 4次过度绘制 – 这部分的像素点只在屏幕上绘制了五次。
在实际项目中,一般认为蓝色即是可以接受的颜色。
我们来看一个简单却隐藏了很多问题的界面,App的设置界面。在没有优化之前打开Overdraw调试,可以看到界面大多数是严重的红色:见下图。
贴出这个布局的代码(贴出后超过公众号字数限制,可以点击查看原文查看布局代码)。
每一行布局都使用RelativeLayout嵌套来做,而且设置了多重颜色。
分析布局可知:多层布局重复设置了背景色导致Overdraw。 那么我们结合产品的需求(任何不结合具体场景优化都是耍流氓):
备注:一个容易忽略的点是我们的Activity使用的Theme可能会默认的加上背景色,不需要的情况下可以去掉。
去掉背景色之后再看一下Overdraw;
对比一下优化后的布局的颜色,可以看出Overdraw降到了可以接受的程度。
备注:有些过度绘制都是不可避免的,需要结合具体的布局场景具体分析。
由此得到结论:那么随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差。
幸运的是,我们有Hierarchy Viewer这个方便可视化的工具,可以得到:树形结构总览、布局view、每一个View(包含子View)绘制所花费的时间及View总个数。
备注: Hierarchy Viewer不能连接真机的问题可以通过ViewServer这个库解决;
使用Hierarchy Viewer来看查看一下设置界面,可以从下图中得到设置界面的一些数据及存在的问题:
优化方案:
现在我们再使用Hierarchy Viewer来检测一下:
优化后: 1. 控件数量从85个减少到26个,减少69%; 2. 绘制时间从17.8ms减少到14.756ms,降低17%;
总结: 1. 同样的UI效果可以使用不同的布局来完成,我们需要考虑使用少的嵌套层次以及控件个数来完成,例如设置界面的普通一行,可以像之前一样使用RelativeLayout嵌套TextView以及ImageView来实现,但是明显只使用TextView来做:嵌套层次、控件个数都更少。 2. 优化过程中使用低端手机更易发现瓶颈;
根据Android性能优化典范,打开设备的GPU配置渲染工具——》在屏幕上显示为条形图,可以协助我们定位UI渲染问题。
从Android M版本开始,GPU Profiling工具把渲染操作拆解成如下8个详细的步骤进行显示。
备注:GPU配置渲染工具虽然可以定位出问题发生在某个步骤,但是并不能定位到具体的某一行;当我们定位到某个步骤之后可以使用工具TraceView进行更加详细的定位。TraceView的使用可以参照《Android性能优化(一)之启动加速35%》。
merge可以用来合并布局,减少布局的层级。merge多用于替换顶层FrameLayout或者include布局时,用于消除因为引用布局导致的多余嵌套。 例如:需要显示一个Button,布局如下;
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merge标签演示" />
</LinearLayout>
我们通过UiAutoMatorViewer(无需root,相比Hierarchy Viewer只能查看布局层次,不能得到绘制时间)看一下布局的层次
我们使用Merge标签对代码进行修改;
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merge标签演示" />
</merge>
再看下布局的层次:
可以看到使用Merge标签进行优化之后布局嵌套就少了一层,Button作为父视图第三层FrameLayout的直接子视图。
注意:merge标签常用于减少布局嵌套层次,但是只能用于根布局。
推迟创建对象、延迟初始化,不仅可以提高性能,也可以节省内存(初始化对象不被创建)。Android定义了ViewStub类,ViewStub是轻量级且不可见的视图,它没有大小,没有绘制功能,也不参与measure和layout,资源消耗非常低。 1、
<ViewStub
android:id="@+id/mask"
android:layout="@layout/b_me_mask"
android:layout_width="match_parent"
android:layout_height="match_parent" />
ViewStub viewStub = (ViewStub)view.findViewById(R.id.mask);
viewStub.inflate();
App里常见的视图如蒙层、小红点,以及网络错误、没有数据等公共视图,使用频率并不高,如果每一次都参与绘制其实是浪费资源的,都可以借助ViewStub标签进行延迟初始化,仅当使用时才去初始化。
include标签和布局性能关系不大,主要用于布局重用,一般和merge标签配合使用,因和本文主题关联不大,此处不展开讨论。
经过这几步的优化之后,一般就不会再有布局的性能问题,同时还是要强调:优化是一个长期的工作,同时也必须结合具体场景:有取有舍!
参考:Android性能优化典范