IBigerBiger的成长之路

Android性能优化(一)-布局优化

关于性能优化,这是一个经常会被忽视的问题,但是对于一个程序员的成长,对于性能优化是一个必不可少的过程,对于性能优化也会提升我们代码习惯与代码质量,同样也会更多掌握谷歌提供的性能相关工具

性能优化一般从渲染,运算,内存,电量四个方面进行,当然我这里会展开更多的方面去做讲解

这一篇主要是针对于布局优化的,所以我们要先了解Android的渲染机制

渲染机制

我们知道Android系统每隔16ms就重新绘制一次界面,也就是说,每一帧只能停留16ms,所以尽量保证每次在16ms内处理完所有的CPU与GPU计算、绘制、渲染等操作,否则会造成丢帧卡顿问题。

16ms意味着1000/60hz,相当于60fps。人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的。12fps大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。 24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。 但是低于30fps是 无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,超过60fps就没有必要了。如果我们的应用没有在16ms内完成屏幕刷新的全部逻辑操作,就会发生卡顿。

正常情况下Android的渲染机制如下

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSync是Vertical Synchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制。



可以看到正常情况下在16ms内完成了所有操作,那么整个过程如果保证在16ms以内就能达到一个流畅的画面。

如果操作超过了16ms就会发生下面的情况:



可以看到当16ms时候VSYNC发出了,而我们的还在进行相关的操作,这时候不会触发重绘,而是在下一次VSYNC发出的时候才会进行重新绘制,那么这样其实就是32ms才进行一次刷新了,这已经超出了人类大脑与眼睛对一个画面的连贯性感知的界限,所以用户就会发现卡顿的现象

接下来看一下这16秒内做了什么,知道做了什么就可以想出对了的优化方法

渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU 负责Rasterization(栅格化)操作。



Resterization栅格化是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作。

CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。



然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。

下面一张图很好的展示出了CPU和GPU的工作,以及潜在的问题,检测的工具和解决方案。



通过上图可以看到

  • CPU产生的问题:不必要的布局和失效
  • GPU产生的问题:过度绘制(overdraw)

那么接下来就是解决这些问题了,对于这些问题首先是我们需要去通过工具确定问题所在

检测工具

1.过度绘制(overdraw)检测

Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面, 如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。

按照以下步骤打开Show GPU Overrdraw的选项:

设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制

好了,打开以后呢,你会发现屏幕上有各种颜色,此时你可以切换到需要检测的程序,对于各个色块,对比一张Overdraw的参考图:



蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,

  • 蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。

  • 绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。

  • 淡红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。

  • 深红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。

2.Hierarchy Viewer工具

Hierarchy Viewer工具提供了一个可视化界面显示布局的层次结构,让我们可以进行调试,从而优化界面布局结构。

打开Hierarchy Viewer有两种方式

第一种直接在 sdk>tools 下面找到 hierarchyviewer.bat 双击运行。

但是出现这个提示:

The standalone version of hieararchyviewer is deprecated.Please use Android Device Monitor (tools/monitor.bat) instead.

大概意思是说,单独版本的hieararchyviewer已经被弃用了。请使用Android Device Monitor来代替。

所以这里我们用第二种方式通过Android Device Monitor来使用hieararchyviewer,其实只是后面的版本将hierarchyviewer整合到了Android Device Monitor里面了而已

打开流程如下图



启动 Android Device Monitor 成功之后,在新的的窗口中点击切换视图图标,选择 Hierarchy Viewe ,如下图:



如果你是用的模拟器或者开发版手机的话则可以直接进行连接调试了,如果不是的话,官方提供了两种方式,进行连接真机调试:

1.通过第三方库,安装和配置ViewServer,配置步骤比较简单,主要分为如下三步:

  • 添加依赖,外层build.gradle文件,添加工具远程仓库地址;内层build.gradle文件,添加依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //外层build.gradle
    allprojects {
    repositories {
    jcenter()
    maven { url "https://jitpack.io" }
    }
    }
    //内层build.gradle
    dependencies {
    ...................................
    compile 'com.github.romainguy:ViewServer:017c01cd512cac3ec054d9eee05fc48c5a9d2de'
    }
  • 在manifest文件申请网络权限

    1
    <uses-permission android:name="android.permission.INTERNET"/>
  • 在调试的Activity以下该三个方法中添加几行代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Set content view, etc.
    ViewServer.get(this).addWindow(this);
    }
    public void onDestroy() {
    super.onDestroy();
    ViewServer.get(this).removeWindow(this);
    }
    public void onResume() {
    super.onResume();
    ViewServer.get(this).setFocusedWindow(this);
    }

    2.只需在环境变量添加这句话(以Mac系统为例),然后启动HierarchyViewer即可:

    1
    export ANDROID_HVPROTO=ddm

    这样设置的原理是,让HV使用与ddms相同的协议来连接设备。也就是说,为了运行HV,需要杀掉其他ddms会话进程。

    但是网上有反馈说部分机型不适用,所以还是选择第一种方法比较靠谱些。

当可以连接调试后,选择一个节点,点击右上角的按钮,就可以获取到布局绘制的时间,如下所示



这里我们主要关注下面的三个圆圈,从左到右依次,代表View的Measure, Layout和Draw的性能,不同颜色代表不同的性能等级:

  • 绿: 表示该View的此项性能比该View Tree中超过50%的View都要快;例如,一个绿点的测量时间意味着这个视图的测量时间快于树中的视图对象的50%。

  • 黄: 表示该View的此项性能比该View Tree中超过50%的View都要慢;例如,一个黄点布局意味着这种观点有较慢的布局时间超过50%的树视图对象。

  • 红: 表示该View的此项性能是View Tree中最慢的;例如,一个红点的绘制时间意味着花费时间最多的这一观点在树上画所有的视图对象。

如果你发现某个View上的圈是红色,那么说明这个View相对其他的View,该操作运行最慢,注意只是相对别的View,并不是说就一定很慢。

布局优化

一.布局的抉择

在开发中,我们一般最常用的布局是RelativeLayout和LinearLayout

那么首先我们需要了解下关于RelativeLayout与LinearLayout的性能开销问题,这个网上已经有开发者做出了相对的分析过程了,那么我就不重复造轮子了,那么这里直接贴上来对比结果与结论

相同效果不同布局的性能对比

LinearLayout

Measure:0.738ms
Layout:0.176ms
draw:7.655ms

RelativeLayout

Measure:2.280ms
Layout:0.153ms
draw:7.696ms

对比结论:

RelativeLayout比LinearLayout性能消耗更大,原因在于RelativeLayout会让子View调用2次onMeasure,LinearLayout则是只有在某个子View有weight时,会让该子View调用2次onMeasure。

所以是不是我们选择用LinearLayout而不选择RelativeLayout就可以提升性能呢?

答案当然是否定的,因为使用LinearLayout可能会导致层级的加重,对于嵌套的层级越深,那么布局其实是相对来说更加复杂的,自然用于onMeasure,onLayout与onDraw的时间就会更长,所以这样其实性能反而并没有提升。

所以对于布局的抉择其实是根据实际情况来做选择的,我这里仅仅提出几点需要注意的点

  • 尽量不要在嵌套的LinearLayout中都使用weight属性,这样会使所有的子View都会进行2次onMeasure,导致性能下降。

  • RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。(这个也是来自性能对比结论之一)

  • 在不影响View树层级的前提下,尽量用LinearLayout去替代RelativeLayout,当然使用LinearLayout也要注意第一点。

  • 当布局使用两个LinearLayout时候,可以考虑使用一个RelativeLayout去替代,在时间上此时RelativeLayout耗时要比两个LinearLayout小。

  • 当使用Hierarchy Viewer查看发现布局的层级超过5层时候, 真的就需要考虑优化下布局了

二.减少View

举个简单例子:

在布局中Icon+文字组成一个视觉上的元素是非常普遍的需求,或者是成为一个可点击的整体,或者是展示某种信息。这种需求实现起来也是非常的简单,初学安卓的同学也能很快的写出来,一个ImageView加上一个TextView,外面再包一层LinearLayout或者RelativeLayout。这需要三个View对象,一个外层group,一个ImageView一个TextView。那么我们有没有更好的解决方案呢?

其实仅仅用一个TextView就可以实现上面的效果

TextView的drawable属性

TextView有一些属性可以在Text的四周设置一个drawable对象,图片,shape等合法的drawable都可以用。

  • drawableStart API 14才有
  • drawableLeft
  • drawableTop
  • drawableBottom
  • drawableRight
  • drawableEnd API 14才有
  • drawablePadding 用以设置drawable与text之间的空间

它们的含义就像其名字所暗示的那样,left/top/right/bottom就是在文字的上下左右放置drawable。而drawableStart和drawableEnd则有特殊的意义,虽然它们是API 14加上去的,但是要在API 17后才能真正的生效,它们的作用是当语言方向发生变化时,会换边,LTR语言drawableStart在左边,而drawableEnd在右边;但对于RTL语言来说就会反过来drawableStart在右,drawableEnd在左。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_launcher"
android:drawableLeft="@drawable/ic_launcher"
android:textSize="16sp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:gravity="center"
android:drawablePadding="5dp"
android:text="Text along with Image"
/>

设置效果如下



当然这种方式也有可能并不能满足我们的需求,这就需要开发者时刻注意开发过程中的细节,用极客的态度时时刻刻想着怎么去降低层级,减少View。

三.移除不必要的背景

通过一个例子(来源Google性能优化)来说明,我们来实现一个列表

mian布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:background="@android:color/white"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/narrow_space"
android:textSize="@dimen/large_text_size"
android:layout_marginBottom="@dimen/wide_space"
android:text="@string/header_text"/>
<ListView
android:id="@+id/id_listview_chats"
android:layout_width="match_parent"
android:background="@android:color/white"
android:layout_height="wrap_content"
android:divider="@android:color/transparent"
android:dividerHeight="@dimen/divider_height"/>
</LinearLayout>

item布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?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="horizontal"
android:paddingBottom="@dimen/chat_padding_bottom">
<ImageView
android:id="@+id/id_chat_icon"
android:layout_width="@dimen/avatar_dimen"
android:layout_height="@dimen/avatar_dimen"
android:src="@drawable/joanna"
android:layout_margin="@dimen/avatar_layout_margin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:orientation="vertical">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:textColor="#78A"
android:orientation="horizontal">
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:padding="@dimen/narrow_space"
android:text="@string/hello_world"
android:gravity="bottom"
android:id="@+id/id_chat_name" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textStyle="italic"
android:text="@string/hello_world"
android:padding="@dimen/narrow_space"
android:id="@+id/id_chat_date" />
</RelativeLayout>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/narrow_space"
android:background="@android:color/white"
android:text="@string/hello_world"
android:id="@+id/id_chat_msg" />
</LinearLayout>
</LinearLayout>

运行后效果如下



我们从布局看层级不深,应该是没有什么问题的吧,但是我们打开Show GPU Overrdraw的时候,惊呆了



Overrdraw的情况特别严重,一个简单的列表展示Item,竟然很多地方被过度绘制了4X,那么为什么呢?明明布局的层级不深,其实主要原因是由于该布局文件中存在很多不必要的背景,多一层背景就会多一次的Overdraw,带来绘制性能损耗。

那么这里分析上述的布局文件,移除没有必要的背景。

  • 不必要的Background 1

    我们主布局的文件已经是background为white了,那么可以移除ListView的白色背景

  • 不必要的Background 2

    Item布局中的LinearLayout的android:background=”@android:color/darker_gray”

  • 不必要的Background 3

    Item布局中的RelativeLayout的android:background=”@android:color/white”

  • 不必要的Background 4

    Item布局中id为id_msg的TextView的android:background=”@android:color/white”

除了这些地方以外呢?其实还有一个很容易被忽视的地方

当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用getWindow().setBackgroundDrawable(null);或者在theme中添加android:windowbackground=”null”;

经过这五个地方的设置我们看下优化后的Overdraw情况



这样其实是到达一个比较好的情况,Overdraw情况良好。

我们在开发过程中的一些习惯性思维定式会带来不经意的Overdraw,所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。

四.善于使用布局标签
1.include标签

include标签常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:background="@color/app_bg"
android:gravity="center_horizontal">
<include layout="@layout/titlebar"/>
<TextView android:layout_width=”match_parent”
android:layout_height="wrap_content"
android:text="@string/hello"
android:padding="10dp" />
...
</LinearLayout>
2.merge标签

merge标签在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。merge多用于替换FrameLayout或者当一个布局包含另一个时,merge标签消除视图层次结构中多余的视图组。例如你的主布局文件是垂直布局,引入了一个垂直布局的include,这是如果include布局使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用merge标签优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/add"/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/delete"/>
</merge>
3.ViewStub标签

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

1
2
3
4
5
6
7
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/progress_overlay"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />

当你想加载布局时,可以使用下面其中一种方法:

1
2
3
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

当调用inflate()函数的时候,ViewStub被引用的资源替代,并且返回引用的view。这样程序可以直接得到引用的view而不用再次调用函数findViewById()来查找了。

当然除了上述的几个优化方式,其实在我们布局的时候还有很多可以注意的点,就不做更多的介绍了。