• 10.1 布局优化" level="2">10.1 布局优化

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 布局优化 - 图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.1 布局优化 - 图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组件中才有的属性。请看一个非常简单的布局文件,代码如下所示。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="0dp"
  4. android:layout_height="0dp"
  5. android:textSize="30sp" android:gravity="center"
  6. android:text="this is a common ui">
  7.  
  8. </TextView>

代码中只有一个TextView,并居中显示一段文字,但更多的时候,我们都会通过相应的Layout组件将若干个控件进行组合成一个共通UI。为了简化操作,这里暂且把它作为一个共通UI。在代码中,将layout_width和layout_height设置为0dp,这样就迫使开发者在使用时对宽高进行赋值,否则将无法看见这个界面。

那么如何使用这个共通UI呢?非常简单,只需要在使用该共通UI的布局文件中通过<include>标签的layout属性添加对这个共通UI的ID的引用即可,代码如下所示。

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:paddingLeft="@dimen/activity_horizontal_margin"
  6. android:paddingRight="@dimen/activity_horizontal_margin"
  7. android:paddingTop="@dimen/activity_vertical_margin"
  8. android:paddingBottom="@dimen/activity_vertical_margin"
  9. tools:context=".MainActivity">
  10.  
  11. <TextView
  12. android:text="@string/hello_world"
  13. android:layout_width="wrap_content"
  14. android:layout_height="wrap_content" />
  15.  
  16. <include layout="@layout/common_ui"
  17. android:layout_alignParentBottom="true"
  18. android:layout_width="match_parent"
  19. android:layout_height="wrap_content"/>
  20.  
  21. </RelativeLayout>

通过上面的代码我们可以发现,在<include>标签中,同样可以使用Layout组件的一些属性来控制引用的布局。不过这里有一点非常需要注意,如果你需要在<include>标签中覆盖类似原布局中android:layout_XXXXX的属性,就必须在<include>标签中同时指定android:layout_width和android:layout_height属性。实现效果如图10.3所示。

10.1 布局优化 - 图3 图10.3 引用布局

10.1.4.2 使用<ViewStub>实现View的延迟加载

除了把一个View作为共通UI,并通过<include>标签来进行引用之外,还可以使用<ViewStub>标签来实现对一个View的引用并实现延迟加载。<ViewStub>是一个非常轻量级的组件,它不仅不可视,而且大小为0。下面同样通过一个实例来演示下如何使用<ViewStub>。

首先,创建一个布局,这个布局在初始化加载时不需要显示,只在某些情况下才显示出来,例如查看用户信息时,只有点击某个按钮时,用户详细信息才显示出来。我们写一个简单的布局文件,代码如下所示。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6.  
  7. <TextView
  8. android:id="@+id/tv"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:text="not often use layout"
  12. android:textSize="30sp" />
  13. </LinearLayout>

接下来,与使用<include>标签类似,在主布局中的<ViewStub>中的layout属性来引用上面的布局,代码如下所示。

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:paddingLeft="@dimen/activity_horizontal_margin"
  6. android:paddingRight="@dimen/activity_horizontal_margin"
  7. android:paddingTop="@dimen/activity_vertical_margin"
  8. android:paddingBottom="@dimen/activity_vertical_margin"
  9. tools:context=".MainActivity">
  10.  
  11. <Button
  12. android:text="Visible"
  13. android:layout_width="wrap_content"
  14. android:layout_height="wrap_content"
  15. android:onClick="btnVisible"
  16. android:id="@+id/button" />
  17.  
  18. <Button
  19. android:layout_width="wrap_content"
  20. android:layout_height="wrap_content"
  21. android:text="Inflate"
  22. android:onClick="btnInflate"
  23. android:id="@+id/button2"
  24. android:layout_alignBottom="@+id/button"
  25. android:layout_toEndOf="@+id/button" />
  26.  
  27. <ViewStub
  28. android:id="@+id/not_often_use"
  29. android:layout_alignParentBottom="true"
  30. android:layout_width="match_parent"
  31. android:layout_height="wrap_content"
  32. android:layout="@layout/not_often_use" />
  33.  
  34. </RelativeLayout>

运行程序后,我们发现,<ViewStub>标签中引用的布局的确没有显示出来,那么如何重新加载显示的布局呢?

首先,通过普通的findViewById()方法找到<ViewStub>组件,这点与一般的组件基本相同,代码如下所示。

  1. mViewStub = (ViewStub) findViewById(R.id.not_often_use);

接下来,有两种方式来重新显示这个View。

  • VISIBLE

通过调用ViewStub的setVisibility()方法来显示这个View,代码如下所示。

  1. mViewStub.setVisibility(View.VISIBLE);
  • inflate

通过调用ViewStub的inflate()方法来显示这个View,代码如下所示。

  1. View inflateView = mViewStub.inflate();

这两种方式都可以让ViewStub重新展开,显示引用的布局,而唯一的区别就是inflate()方法可以返回引用的布局,从而可以再通过View. findViewById()方法来找到对应的控件,代码如下所示。

  1. View inflateView = mViewStub.inflate();
  2. TextView textView = (TextView) inflateView.findViewById(R.id.tv);
  3. 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.1 布局优化 - 图4 图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,有兴趣的读者朋友可以去如下所示的网址了解一下。

  1. https://github.com/romainguy/ViewServer

下面在模拟器中使用这个工具,它位于sdk\tools目录下,在命令行中输入hierarchyviewer.bat启动程序,如图10.5所示。

10.1 布局优化 - 图5 图10.5 启动hierarchyviewer工具

启动后的界面如图10.6所示。

10.1 布局优化 - 图6 图10.6 hierarchyviewer初始界面

选择要调试的进程,即测试进程,然后点击上面的“Load View Hierarchy”按钮,显示界面如图10.7所示。

10.1 布局优化 - 图7 图10.7 hierarchyviewer界面

为了测试这个工具,我们写了一个非常冗余的布局文件,代码如下所示。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6.  
  7. <LinearLayout
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent">
  10.  
  11. <LinearLayout
  12. android:layout_width="match_parent"
  13. android:layout_height="match_parent">
  14.  
  15. <Button
  16. android:layout_width="wrap_content"
  17. android:layout_height="wrap_content"
  18. android:text="hierarchyviewer"/>
  19. </LinearLayout>
  20. </LinearLayout>
  21. </LinearLayout>

读者朋友可以发现,使用三层LinearLayout嵌套,只装载了一个Button,很显然这些LinearLayout都是冗余的。下面利用hierarchyviewer工具,使用前面所说的方法打开这个布局,其显示结果就是图10.5所示的内容。

通常情况下,重点关注ID为content的FrameLayout的分支,这也是setContentView()所设置的内容,如图10.8所示。

10.1 布局优化 - 图8 图10.8 布局内容

在这里可以看见三层LinearLayout,而且这三层LinearLayout都没有任何分支。这说明了这些LinearLayout都是可以直接去掉的,这与我们的分析也是一样的。

当点击其中一个View的时候,可以显示该View的绘制情况。不过,第一次点击的时候,各种显示的时间都将是NA,需要点击菜单中的“Profile Node”按钮重新进行计算,才能获取绘制信息,如图10.9所示。

10.1 布局优化 - 图9 图10.9 绘制信息

此时就可以知道每个View所绘制的时长,并且系统在下方也给出了三个不同颜色的小圆点,用来表示绘制的效率,绿、黄、红分别代表好、中、差三种不同的绘制效率。

通过hierarchyviewer工具,就可以很快地在视图树中找到冗余的布局,从而有目的地优化布局。同时,hierarchyviewer工具还可以显示很多有用的信息,如图10.10所示。或者还可以将视图树dump出来进行进一步的分析,如图10.11所示。

10.1 布局优化 - 图10 10.1 布局优化 - 图11
图10.10 显示布局相关属性 图10.11 更多的操作

总之,hierarchyviewer是进行布局优化的一个非常有用的工具,大家可以在官方API文档中找到更多更详细的使用教程。