App优化之Layout怎么摆

优化完App的启动速度, 接下来我们要关注的就是UI布局怎么更高效了.

欲善其事, 先利其器. 分析布局, 就不得不用到Hierarchy Viewer了.

本文工具使用皆以GithubApp的详情界面RepoDetailActivity为例说明.
为了不影响阅读体验, 对应的布局文件activity_repo_detail.xml的代码放在文末

1, Hierarchy Viewer怎么用

Hierarchy发音 [美: ‘haɪərɑrki] [英: ‘haɪərɑːkɪ] 层次结构的意思.
之前一直念不顺这个单词Hierarchy, 就简称为H Viewer了. 下文就这么简称吧.

官网描述, H Viewer是用来分析调试和优化我们的UI的一个图形化工具. 它会展示当前界面的View层级.

1.1 启用H Viewer

比较早接触Android开发的同学可能知道, H Viewer只能在root过的机器才能使用. 主要是在没有root过的机器中view server这个服务是没有开启的. H Viewer就无法连接到机器获取view层级信息.

正所谓高手在民间, 大家都尝试在未root的机器中启用view server来使用H Viewer. 最具代表性的就是romainguy的ViewServer, 只需集成少量代码到你的Activity, 相当于在手机端开启了view server服务, 建立socket通道与PC端的H Viewer通信.

此工程被Android官网吸收, 作为开启H View的方案之一.

完整开启H Viewer的套路如下:

  1. 手机开启开发者模式, USB调试.
  2. 根据手机的Android系统版本:
    • 4.0及以下, 没有root. 使用上述的开源工程ViewServer提供的方式.
    • 4.0及以下, 已经root. 无需其他额外设置.
    • 4.1及以上. 需要在PC端设置ANDROID_HVPROTO环境变量.

设置系统环境变量: ANDROID_HVPROTO, 值为ddm
具体设置系统环境变量根据PC系统不同而异.

做完上述配置后, 你就可以打开H Viewer了, 打开DDMS, 如下操作进入H Viewer界面:
ddms_open_hviewer

1.2 H Viewer界面详解

GithubApp的详情界面RepoDetailActivity为例说明:
Snip20160902_1

界面分为四个部分:

  1. Window
    显示当前连接的设备和供分析的界面. 可手动选择.

  2. Tree View
    树状图的形式展示该Activity中的View层级结构. 可以放大缩小, 每个节点代表一个View, 点击可以弹出其属性, 当前值, 并且在LayoutView中会显示其在界面中相应位置.
    Tree View是我们主要要分析的视图.

  3. Tree Overview
    Tree View的概览图. 有一个选择框, 可以拖动选择查看. 选中的部分会在Tree View中显示.

  4. Layout View
    匹配手机屏幕的视图, 按照View的实际显示位置展示出来的框图.

1.3 H Viewer参数解读

  1. 通过Tree View可以很直观的看到View的层级.
  2. 点击Tree View的RepoItemView这个节点:

关于三个小圆点的性能指示, 在App优化之性能分析工具一文中有提到, 再强调一遍:

三个小圆点, 依次表示Measure, Layout, Draw, 可以理解为对应View的onMeasure, onLayout, onDraw三个方法.

  • 绿色, 表示该View的此项性能比该View Tree中超过50%的View都要快.
  • 黄色, 表示该View的此项性能比该View Tree中超过50%的View都要慢.
  • 红色, 表示该View的此项性能是View Tree中最慢的.

如果你的界面的Tree View中红点较多, 那就需要注意了. 一般来说:

1, Measure红点, 可能是布局中嵌套RelativeLayout, 或是嵌套LinearLayout都使用了weight属性.
2, Layout红点, 可能是布局层级太深.
3, Draw红点, 可能是自定义View的绘制有问题, 复杂计算等.

由上图, 可以看到我们的RepoItemView的三项指标都不合格, 证明其还有很多优化空间. 层级, 绘制都可以优化.

除了用H Viewer来做代码后分析, Android还提供了Lint, 在我们编写xml布局文件时就即时的给出一些相关提示.

2, Lint tool

打开RepoDetailActivity的布局文件activity_repo_detail.xml, 在Android Studio菜单栏中开启Lint检查:

选择当前文件:

会在下方弹出分析结果:

分析结果包括用法检测(例如版本特有属性), 国际化(字符串是否提取到strings.xml, Rlt支持等), 以及我们今天的主题—性能分析结果.

点开”Android -> Lint -> Performance”项, 可以看到关于布局性能的建议项. 此例中是说ScrollView的父级LinearLayout是不必要的.

3, 怎么优化你的布局

通过以上工具的使用和分析, 也基本能找到布局的一些常见的好与不好的了.

正所谓授之以鱼不如授之以渔. 在此也就不太详细去讲怎么优化了, 几点建议, 大家自行实践吧:)

尽量减少布局层级和复杂度

  1. 尽量不要嵌套使用RelativeLayout.
  2. 尽量不要在嵌套的LinearLayout中都使用weight属性.
  3. Layout的选择, 以尽量减少View树的层级为主.
  4. 去除不必要的父布局.
  5. 善用TextView的Drawable减少布局层级.
  6. 如果H Viewer查看层级超过5层, 你就需要考虑优化下布局了~

善用Tag


  1. 使用include来重用布局.

  2. 使用来解决include或自定义组合ViewGroup导致的冗余层级问题. 例如本例中的RepoItemView的布局文件实际可以用一个标签来减少一级.

ListView优化

  1. contentView复用
  2. 引入holder来避免重复的findViewById.
  3. 分页加载

4, 附示例代码

因github上的源码会持续更新, 特留对应代码在此.

activity_repo_detail.xml

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/root_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/md_white_1000"
android:orientation="vertical"
android:padding="@dimen/dimen_10">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.anly.githubapp.ui.widget.RepoItemView
android:id="@+id/repo_item_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/md_grey_300"
android:elevation="@dimen/dimen_2"/>
<LinearLayout
android:id="@+id/contributor_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_10"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="@drawable/button_bg"
android:paddingLeft="@dimen/dimen_10">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="{oct-organization} Contributors"/>
<TextView
android:id="@+id/contributors_count"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/contributor_list"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_60"
android:layout_marginTop="@dimen/dimen_2"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/fork_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_10"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="@drawable/button_bg"
android:paddingLeft="@dimen/dimen_10"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="{oct-gist_fork} Forks"/>
<TextView
android:id="@+id/forks_count"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/fork_list"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_60"
android:layout_marginTop="@dimen/dimen_2"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/code_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_marginTop="@dimen/dimen_10"
android:background="@drawable/button_bg"
android:paddingLeft="@dimen/dimen_10">
<TextView
android:id="@+id/code_label"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"
android:text="{oct-file_code} Code"/>
</LinearLayout>
<LinearLayout
android:id="@+id/readme_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_marginTop="@dimen/dimen_10"
android:background="@drawable/button_bg"
android:paddingLeft="@dimen/dimen_10">
<TextView
android:id="@+id/readme_label"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_40"
android:gravity="center_vertical"
android:text="{oct-info} README"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

com.anly.githubapp.ui.widget.RepoItemView对应的布局:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen_10">
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="left|center_vertical"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_18"/>
<TextView
android:id="@+id/desc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="left|center_vertical"
android:maxLines="2"
android:text="@string/app_name"
android:textColor="@android:color/darker_gray"
android:textSize="@dimen/text_size_12"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_5"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/image"
android:layout_width="@dimen/dimen_32"
android:layout_height="@dimen/dimen_32"
android:scaleType="centerInside"
android:src="@mipmap/ic_launcher"
android:visibility="visible"/>
<TextView
android:id="@+id/owner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/dimen_10"
android:gravity="left|center_vertical"
android:text="@string/app_name"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_14"/>
</LinearLayout>
<View
android:layout_marginTop="@dimen/dimen_5"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/grey"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_32"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="@dimen/dimen_10">
<TextView
android:id="@+id/update_time"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="left|center_vertical"
android:text="@string/app_name"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_12"
/>
<View
android:layout_width="1px"
android:layout_height="match_parent"
android:background="@color/grey"/>
<LinearLayout
android:id="@+id/star_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/star_icon"
android:layout_width="@dimen/dimen_16"
android:layout_height="@dimen/dimen_16"
android:scaleType="centerInside"
android:src="@drawable/ic_star"/>
<TextView
android:id="@+id/star"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/dimen_5"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_12"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<com.flyco.labelview.LabelView
android:id="@+id/label_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
app:lv_background_color="@color/md_yellow_500"
app:lv_gravity="TOP_RIGHT"
app:lv_text="TEST"
app:lv_text_size="@dimen/text_size_12"/>
</FrameLayout>

优化不同于做功能, 可能分析的多, 出的成果少~ 比较枯燥, 然而优化也是App发展的必经之路, 欢迎大家分享经验.