10.1 布局优化" class="reference-link">10.1 布局优化
系统在渲染UI界面的时候将消耗大量的资源,一个好的UI不仅应该具有良好的视觉效果,更应该具有良好的使用体验,因此布局优化就显得非常重要。
10.1.1 Android UI渲染机制" class="reference-link">10.1.1 Android UI渲染机制
人眼所感觉的流畅画面,需要画面的帧数达到40帧每秒到60帧每秒,相信玩过PC游戏的朋友应该对帧数的概念非常清楚,最佳fps大概在60fps左右,这也是评价一个显卡性能高低的标准之一。在Android中,系统通过VSYNC信号触发对UI的渲染、重绘,其间隔时间是16ms。这个16ms其实就是1000ms中显示60帧画面的单位时间,即1000/60。如果系统每次渲染的时间都保持在16ms之内,那么我们看见的UI界面将是非常流畅的,但这也就需要将所有程序的逻辑都保证在16ms内。如果不能在16ms内完成绘制,那么就会造成丢帧现象,即当前该重绘的帧被未完成的逻辑阻塞,例如一次绘制任务耗时20ms,那么在16ms系统发出VSYNC信号时就无法绘制,该帧就被丢弃,等待下次信号才开始绘制,导致16*2ms内都显示同一帧画面,这就是画面卡顿的原因。
Android系统提供了检测UI渲染时间的工具,打开“开发者选项”,选择“Profile GPU Rendering”,并选中“On screen as bars”的选项,这时候在屏幕上将显示一些条形图,如图10.1所示。
图10.1 Profile GPU Rendering
每一条柱状线都包含三部分,蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。中间的绿色横线代表VSYNC时间16ms,需要尽量将所有条形图都控制在这条绿线之下。
10.1.2 避免Overdraw" class="reference-link">10.1.2 避免Overdraw
Overdraw,过度绘制会浪费很多的CPU、GPU资源,例如系统默认会绘制Activity的背景,而如果再给布局绘制了重叠的背景,那么默认Activity的背景就属于无效的过度绘 制——Overdraw。Android系统在开发者选项中提供了这样一个检测工具——“Enable GPU Overdraw”。激活后,可以通过界面上的颜色来判断Overdraw的次数,这里借助Google开发者博客上的一张图来演示,如图10.2所示。
图10.2 Enable GPU Overdraw
通过这个工具可以查看当前区域中的绘制次数,从而尽量优化绘图层次,尽量增大蓝色的区域、减少红色区域。
10.1.3 优化布局层级" class="reference-link">10.1.3 优化布局层级
在Android中,系统对View进行测量、布局和绘制时,都是通过对View数的遍历来进行操作的。如果一个View树的高度太高,就会严重影响测量、布局和绘制的速度,因此优化布局的第一个方法就是降低View树的高度,Google也在其API文档中建议View树的高度不宜超过10层。
不知道是否有读者注意到,在早期的Android版本中,Google使用LineraLayout作为默认创建的XML文件的根布局,而现在版本的Android中,Google已经使用RelativeLayout来替代LineraLayout作为默认的根布局,其原因就是通过扁平的RelativeLayout来降低通过LineraLayout嵌套所产生布局树的高度,从而提高UI渲染的效率。
10.1.4 避免嵌套过多无用布局" class="reference-link">10.1.4 避免嵌套过多无用布局
嵌套的布局会让View树的高度变得越来越高,因此在布局时,需要根据自身布局的特点来选择不同的Layout组件,从而避免通过某一种Layout组件来实现功能时的局限性,从而造成嵌套过多的情况发生。
10.1.4.1 使用<include>标签重用Layout
在一个应用程序界面中,为了风格上的统一,很多界面都会存在一些共通的UI,比如一个应用的Topbar、Bottombar等。对于这些共通的UI,如果在每个界面中都来复制一段这样的代码,不仅不利于后期代码的维护,更增加了程序的冗余度。因此,可以使用<include>标签来定义这样一个共通UI。
下面我们就演示一下如何使用<include>标签。为了能够在不同的Layout组件中使用共通UI,所以最好不要在共通UI中写太多只有某个特定Layout组件中才有的属性。请看一个非常简单的布局文件,代码如下所示。
- <?xml version="1.0" encoding="utf-8"?>
- <TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:textSize="30sp" android:gravity="center"
- android:text="this is a common ui">
- </TextView>
代码中只有一个TextView,并居中显示一段文字,但更多的时候,我们都会通过相应的Layout组件将若干个控件进行组合成一个共通UI。为了简化操作,这里暂且把它作为一个共通UI。在代码中,将layout_width和layout_height设置为0dp,这样就迫使开发者在使用时对宽高进行赋值,否则将无法看见这个界面。
那么如何使用这个共通UI呢?非常简单,只需要在使用该共通UI的布局文件中通过<include>标签的layout属性添加对这个共通UI的ID的引用即可,代码如下所示。
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
- android:paddingBottom="@dimen/activity_vertical_margin"
- tools:context=".MainActivity">
- <TextView
- android:text="@string/hello_world"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- <include layout="@layout/common_ui"
- android:layout_alignParentBottom="true"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- </RelativeLayout>
通过上面的代码我们可以发现,在<include>标签中,同样可以使用Layout组件的一些属性来控制引用的布局。不过这里有一点非常需要注意,如果你需要在<include>标签中覆盖类似原布局中android:layout_XXXXX的属性,就必须在<include>标签中同时指定android:layout_width和android:layout_height属性。实现效果如图10.3所示。
图10.3 引用布局
10.1.4.2 使用<ViewStub>实现View的延迟加载
除了把一个View作为共通UI,并通过<include>标签来进行引用之外,还可以使用<ViewStub>标签来实现对一个View的引用并实现延迟加载。<ViewStub>是一个非常轻量级的组件,它不仅不可视,而且大小为0。下面同样通过一个实例来演示下如何使用<ViewStub>。
首先,创建一个布局,这个布局在初始化加载时不需要显示,只在某些情况下才显示出来,例如查看用户信息时,只有点击某个按钮时,用户详细信息才显示出来。我们写一个简单的布局文件,代码如下所示。
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <TextView
- android:id="@+id/tv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="not often use layout"
- android:textSize="30sp" />
- </LinearLayout>
接下来,与使用<include>标签类似,在主布局中的<ViewStub>中的layout属性来引用上面的布局,代码如下所示。
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
- android:paddingBottom="@dimen/activity_vertical_margin"
- tools:context=".MainActivity">
- <Button
- android:text="Visible"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="btnVisible"
- android:id="@+id/button" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Inflate"
- android:onClick="btnInflate"
- android:id="@+id/button2"
- android:layout_alignBottom="@+id/button"
- android:layout_toEndOf="@+id/button" />
- <ViewStub
- android:id="@+id/not_often_use"
- android:layout_alignParentBottom="true"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout="@layout/not_often_use" />
- </RelativeLayout>
运行程序后,我们发现,<ViewStub>标签中引用的布局的确没有显示出来,那么如何重新加载显示的布局呢?
首先,通过普通的findViewById()方法找到<ViewStub>组件,这点与一般的组件基本相同,代码如下所示。
- mViewStub = (ViewStub) findViewById(R.id.not_often_use);
接下来,有两种方式来重新显示这个View。
- VISIBLE
通过调用ViewStub的setVisibility()方法来显示这个View,代码如下所示。
- mViewStub.setVisibility(View.VISIBLE);
- inflate
通过调用ViewStub的inflate()方法来显示这个View,代码如下所示。
- View inflateView = mViewStub.inflate();
这两种方式都可以让ViewStub重新展开,显示引用的布局,而唯一的区别就是inflate()方法可以返回引用的布局,从而可以再通过View. findViewById()方法来找到对应的控件,代码如下所示。
- View inflateView = mViewStub.inflate();
- TextView textView = (TextView) inflateView.findViewById(R.id.tv);
- textView.setText("Haha!");
而不管使用哪种方式,一旦<ViewStub>被设置为可见或是被inflate了,<ViewStub>就不存在了,取而代之的是被inflate的Layout,并将这个Layout的ID重新设置为<ViewStub>中通过android:inflatedId属性所指定的ID,这也是为什么两次调用inflate方法会报错的原因。
整个程序运行后的效果如图10.4所示。
看到这里,有读者可能有疑问了,<ViewStub>标签与设置View.GONE这种方式来隐藏一个View有什么区别呢?的确,它们的共同点都是初始时不会显示,但是<ViewStub>标签只会在显示时,才去渲染整个布局,而View.GONE,在初始化布局树的时候就已经添加在布局树上了,相比之下<ViewStub>标签的布局具有更高的效率。
图10.4 程序运行效果
10.1.5 Hierarchy Viewer" class="reference-link">10.1.5 Hierarchy Viewer
无论是哪本讲解布局优化的参考书,它们都不得不提到Hierarchy Viewer。不过,通常情况下,Hierarchy Viewer无法在真机上进行使用,它只能在工厂的Demo机和模拟器上使用,即非加密过的设备。Google的“大神”——Romain Guy提供了一个开源项目View Server,通过这个程序可以让普通的手机也能使用Hierarchy Viewer,有兴趣的读者朋友可以去如下所示的网址了解一下。
- https://github.com/romainguy/ViewServer
下面在模拟器中使用这个工具,它位于sdk\tools目录下,在命令行中输入hierarchyviewer.bat启动程序,如图10.5所示。
图10.5 启动hierarchyviewer工具
启动后的界面如图10.6所示。
图10.6 hierarchyviewer初始界面
选择要调试的进程,即测试进程,然后点击上面的“Load View Hierarchy”按钮,显示界面如图10.7所示。
图10.7 hierarchyviewer界面
为了测试这个工具,我们写了一个非常冗余的布局文件,代码如下所示。
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="hierarchyviewer"/>
- </LinearLayout>
- </LinearLayout>
- </LinearLayout>
读者朋友可以发现,使用三层LinearLayout嵌套,只装载了一个Button,很显然这些LinearLayout都是冗余的。下面利用hierarchyviewer工具,使用前面所说的方法打开这个布局,其显示结果就是图10.5所示的内容。
通常情况下,重点关注ID为content的FrameLayout的分支,这也是setContentView()所设置的内容,如图10.8所示。
图10.8 布局内容
在这里可以看见三层LinearLayout,而且这三层LinearLayout都没有任何分支。这说明了这些LinearLayout都是可以直接去掉的,这与我们的分析也是一样的。
当点击其中一个View的时候,可以显示该View的绘制情况。不过,第一次点击的时候,各种显示的时间都将是NA,需要点击菜单中的“Profile Node”按钮重新进行计算,才能获取绘制信息,如图10.9所示。
图10.9 绘制信息
此时就可以知道每个View所绘制的时长,并且系统在下方也给出了三个不同颜色的小圆点,用来表示绘制的效率,绿、黄、红分别代表好、中、差三种不同的绘制效率。
通过hierarchyviewer工具,就可以很快地在视图树中找到冗余的布局,从而有目的地优化布局。同时,hierarchyviewer工具还可以显示很多有用的信息,如图10.10所示。或者还可以将视图树dump出来进行进一步的分析,如图10.11所示。
![]() | ![]() |
| 图10.10 显示布局相关属性 | 图10.11 更多的操作 |
总之,hierarchyviewer是进行布局优化的一个非常有用的工具,大家可以在官方API文档中找到更多更详细的使用教程。
