Android最佳性能实践(四)——布局优化技巧

在前面几篇文章当中,我们学习了如何通过合理管理内存,以及高性能编码技巧的方式来提升应用程序的性能。然而实际上界面布局也会对应用程序的性能产生比较大的影响,如果布局写得糟糕的话,那么程序加载UI的速度就会非常慢,从而造成不好的用户体验。那么本篇文章我们就来学习一下,如何通过优化布局来提供应用程序的性能。还没有看过前面前面一篇文章的朋友建议可以先去阅读 Android最佳性能实践(三)——高性能编码优化 。

重用布局文件

Android系统中已经提供了非常多好用的控件,这让我们在编写布局的时候可以很轻松。但是有些时候我们可能需要反复利用某个已经写好的布局,如果你总是使用复制粘贴的方式来进行布局重用,这显然是一种很笨的做法。而Android当然也已经充分考虑到了布局重用的重要性,于是提供了<include>和<merge>这两个非常有用的标签,下面我们就来逐个学习一下。

<include>

<include>标签可以允许在一个布局当中引入另外一个布局,那么比如说我们程序的所有界面都有一个公共的部分,这个时候最好的做法就是将这个公共的部分提取到一个独立的布局文件当中,然后在每个界面的布局文件当中来引用这个公共的布局。

这里举个例子吧,我们应该都知道,目前几乎所有的软件都会有一个头布局,头布局中可以包含界面的标题、返回按钮、以及其它一些操作功能等。那这样的一个头布局,有些软件是使用ActionBar来实现的,但是由于ActionBar的灵活性不太好,因而也有很多软件会选择自己去编写实现。那如果自己去实现的话,由于这个头布局是在所有界面都要使用的,显然我们不可能在每个界面当中都去写一遍这个头布局的代码,因此这种情况下使用<include>标签就非常合适了。这里为了给大家演示一下,我就编写一个非常简单的头布局,在res/layout文件夹中新建titlebar.xml作为头布局,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <Button  
  7.         android:id="@+id/back"  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:layout_alignParentLeft="true"  
  11.         android:layout_centerVertical="true"  
  12.         android:text="Back" />  
  13.   
  14.     <TextView  
  15.         android:id="@+id/title"  
  16.         android:layout_width="wrap_content"  
  17.         android:layout_height="wrap_content"  
  18.         android:layout_centerInParent="true"  
  19.         android:text="Title"  
  20.         android:textSize="20sp" />  
  21.   
  22.     <Button  
  23.         android:id="@+id/done"  
  24.         android:layout_width="wrap_content"  
  25.         android:layout_height="wrap_content"  
  26.         android:layout_alignParentRight="true"  
  27.         android:layout_centerVertical="true"  
  28.         android:text="Done" />  
  29.   
  30. </RelativeLayout>  
可以看到,titlebar.xml中的布局非常简单,外层是一个RelativeLayout,里面只有两个Button和一个TextView,左边的Button用于实现返回功能,右边的Button用于实现完成功能,中间的TextView则可以用于显示当前界面的标题。我们可以来预览一下titlebar的样子,如下图所示:


好的,那titlebar作为一个独立的布局现在我们已经编写完了,接下来的工作就非常简单了,无论任何界面需要加入titlebar这个功能,只需要在布局文件中引入titlebar.xml就可以了。那么比如说我们的程序当中有一个activity_main.xml文件,现在想要引入titlebar只需要这样写:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <include layout="@layout/titlebar" />  
  8.   
  9.     ......  
  10.   
  11. </LinearLayout>  
非常简单吧,一行include语句就可以搞定了。<include>标签当中可以指定一个layout属性,我们在这个layout属性中填写需要引入的布局名就可以了。而且使用这种引入的方式,以后如果titlebar的界面有所变更,我们只需要修改titlebar.xml这一个文件就可以了,而不是所有界面一个个地去修改。

等等!现在如果你运行一下程序会发现出大问题了,虽然titlebar是成功引入了,但是我们activity_main.xml中本来的界面全部都不见了!出现这个问题是原因是因为titlebar的最外层布局是一个宽高都是match_parent的RelativeLayout,它会将整个布局都填充满,因而我们原本的布局也就看不见了。那既然问题的原因清楚了,相信你立刻就想到应该怎么修改了,将RelativeLayout的layout_height属性修改成wrap_content不就可以了嘛。没错,这样修改当然是没问题的,不过这种修改方式会让所有引用titlebar的界面都受到影响,而如何你只希望让activity_main.xml这一个界面受影响的话,那么可以使用覆写<include>属性的方式。

在<include>标签当中,我们是可以覆写所有layout属性的,即include中指定的layout属性将会覆盖掉titlebar中指定的layout属性。因此,这里我们希望将titlebar的高度设置成wrap_content,就可以这样写:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <include  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         layout="@layout/titlebar" />  
  11.   
  12.     ......  
  13.   
  14. </LinearLayout>  
现在重新运行一下程序应该就可以一切正常了,如下图所示:


除了layout_height之外,我们还可以覆写titlebar中的任何一个layout属性,如layout_gravity、layout_margin等,而非layout属性则无法在<include>标签当中进行覆写。另外需要注意的是,如果我们想要在<include>标签当中覆写layout属性,必须要将layout_width和layout_height这两个属性也进行覆写,否则覆写效果将不会生效。

<merge>

<merge>标签是作为<include>标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。大家都知道,Android去解析和展示一个布局是需要消耗时间的,布局嵌套的越多,那么解析起来就越耗时,性能也就越差,因此我们在编写布局文件时应该让嵌套的层数越少越好。

在上面我们讲解<include>标签的用法时主要介绍了它优点,但是它也存在着一个不好的地方,就是可能会导致产生多余的布局嵌套。这里还是通过举例的方式跟大家说明一下,比如说我们需要编写一个确定取消按钮的公共布局,这样任何一个界面需要确定和取消功能时就不用再单独编写了,新建ok_cancel_layout.xml,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="wrap_content"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <Button  
  8.         android:id="@+id/ok"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:layout_marginLeft="20dp"  
  12.         android:layout_marginRight="20dp"  
  13.         android:text="OK" />  
  14.   
  15.     <Button  
  16.         android:id="@+id/cancel"  
  17.         android:layout_width="match_parent"  
  18.         android:layout_height="wrap_content"  
  19.         android:layout_marginLeft="20dp"  
  20.         android:layout_marginRight="20dp"  
  21.         android:layout_marginTop="10dp"  
  22.         android:text="Cancel" />  
  23.   
  24. </LinearLayout>  
可以看到,这个界面也是非常简单,外层是一个垂直方向的LinearLayout,LinearLayout中包含了两个按钮,一个用于实现确定功能,一个用于实现取消功能。现在我们可以来预览一下这个界面,如下图所示:


好的,然后我们有一个profile.xml的界面需要编辑一些内容,那么这里就可以将ok_cancel_layout这个布局引入到profile.xml界面当中,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.       
  7.     <EditText  
  8.         android:id="@+id/edit"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:layout_marginBottom="10dp"  
  12.         android:layout_marginLeft="20dp"  
  13.         android:layout_marginRight="20dp"  
  14.         android:layout_marginTop="10dp"  
  15.         android:hint="Edit something here" />  
  16.       
  17.     <include layout="@layout/ok_cancel_layout"/>  
  18.   
  19. </LinearLayout>  
在profile.xml当中有一个EditText控件用于编辑内容,然后下面使用了<include>标签来将ok_cancel_layout布局进行引入,现在重新运行一下程序,界面效果如下图所示:


看上去效果非常不错对吗?可是在你毫无察觉的情况下,目前profile.xml这个界面当中其实已经存在着多余的布局嵌套了!感觉还没写几行代码呢,怎么这就已经有多余的布局嵌套了?不信的话我们可以通过View Hierarchy工具来查看一下,如下图所示:


可以看到,最外层首先是一个FrameLayout,这个无可厚非,不知道为什么最外层是FrameLayout的朋友可以去参考 Android LayoutInflater原理分析,带你一步步深入了解View(一) 这篇文章。然后FrameLayout中包含的是一个LinearLayout,这个就是我们在profile.xml中定义的最外层布局。接下来的部分就有问题了,在最外层的LinearLayout当中包含了两个元素,一个是EditText,另一个又是一个LinearLayout,然后在这个内部的LinearLayout当中才包含了确定和取消这两个按钮。

相信大家已经可以看出来了吧,这个内部的LinearLayout就是一个多余的布局嵌套,实际上并不需要这样一层,让两个按钮直接包含在外部的LinearLayout当中就可以了。而这个多余的布局嵌套其实就是由于布局引入所导致的,因为我们在ok_cancel_layout.xml中也定义了一个LinearLayout。那么应该怎样优化掉这个问题呢?当然就是使用<merge>标签来完成了,修改ok_cancel_layout.xml中的代码,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <merge xmlns:android="http://schemas.android.com/apk/res/android">  
  3.   
  4.     <Button  
  5.         android:id="@+id/ok"  
  6.         android:layout_width="match_parent"  
  7.         android:layout_height="wrap_content"  
  8.         android:layout_marginLeft="20dp"  
  9.         android:layout_marginRight="20dp"  
  10.         android:text="OK" />  
  11.   
  12.     <Button  
  13.         android:id="@+id/cancel"  
  14.         android:layout_width="match_parent"  
  15.         android:layout_height="wrap_content"  
  16.         android:layout_marginLeft="20dp"  
  17.         android:layout_marginRight="20dp"  
  18.         android:layout_marginTop="10dp"  
  19.         android:text="Cancel" />  
  20.   
  21. </merge>  
可以看到,这里我们将ok_cancel_layout最外层的LinearLayout布局删除掉,换用了<merge>标签,这就表示当有任何一个地方去include这个布局时,会将<merge>标签内包含的内容直接填充到include的位置,不会再添加任何额外的布局结构。好的,<merge>的用法就是这么简单,现在重新运行一下程序,你会看到界面没有任何改变,然后我们再通过View Hierarchy工具来查看一下当前的View结构,如下图所示:


OK,可以看到,现在EditText和两个按钮都直接包含在了LinearLayout下面,我们的profile.xml当中也就不存在多余的布局嵌套了。

仅在需要时才加载布局

有的时候我们会遇到这样的场景,就是某个布局当中的元素非常多,但并不是所有元素都一起显示出来的,而是普通情况下只显示部分常用的元素,而那些不常用的元素只有在用户进行特定操作的情况下才会显示出来。

这里举个大家都非常熟悉的例子,我们在添加联系人的时候其实可以编辑的字段真的非常多,姓名、电话、email、传真、住址、昵称等等等等,但其实基本上大家最常用的就是填一个姓名,填一个电话而已。那么将这么多繁杂的字段都一起显示在界面上其实并不是一种很好的做法,因为大多数人都是用不到这些字段的。比较聪明的做法就是把最常用的姓名和电话显示在界面上,然后给用户提供一个添加更多字段的选项,当用户真的有需要去添加其它信息的时候,我们才将另外的元素显示到界面上。

说到实现这样一个功能,我相信大多数人的第一反应就是将不常用的元素使用INVISIBLE或者GONE进行隐藏,然后当用户需要使用这些元素的时候再把它们置成VISIBLE显示出来。使用这种方式肯定可以实现功能的,但是性能方面就表现得一般了,因为即使是将元素进行隐藏,它们其实还是在布局当中的,每个元素还拥有着自己的宽、高、背景等等属性,解析布局的时候也会将这些隐藏的元素一一解析出来。

那么我们如何才能让这些不常用的元素仅在需要时才去加载呢?Android为此提供了一种非常轻量级的控件,ViewStub。ViewStub虽说也是View的一种,但是它没有大小,没有绘制功能,也不参与布局,资源消耗非常低,将它放置在布局当中基本可以认为是完全不会影响性能的。

下面我们就来学习一下如何使用ViewStub来完成仅在需要时才去加载布局的功能,目前profile.xml中只有一个EditText用于编辑信息,那么比如说我们还有另外三个不太常用的EditText,就可以将它们定义在另外一个布局文件当中。新建profile_extra.xml文件,代码如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <EditText  
  8.         android:id="@+id/edit_extra1"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:layout_marginLeft="20dp"  
  12.         android:layout_marginRight="20dp"  
  13.         android:hint="Extra field 1" />  
  14.   
  15.     <EditText  
  16.         android:id="@+id/edit_extra2"  
  17.         android:layout_width="match_parent"  
  18.         android:layout_height="wrap_content"  
  19.         android:layout_marginLeft="20dp"  
  20.         android:layout_marginRight="20dp"  
  21.         android:layout_marginTop="10dp"  
  22.         android:hint="Extra field 2" />  
  23.   
  24.     <EditText  
  25.         android:id="@+id/edit_extra3"  
  26.         android:layout_width="match_parent"  
  27.         android:layout_height="wrap_content"  
  28.         android:layout_marginLeft="20dp"  
  29.         android:layout_marginRight="20dp"  
  30.         android:layout_marginTop="10dp"  
  31.         android:hint="Extra field 3" />  
  32.   
  33. </LinearLayout>  
可以看到,在profile_extra.xml这个布局文件当中定义了三个EditText,也就是用于编辑那些不常用信息的控件,现在我们可以来预览一下这个布局,如下图所示:


目前profile_extra.xml是一个独立的布局,和profile.xml这个布局文件是完全没有关系的。接下来我们修改profile.xml文件中的代码,如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <EditText  
  8.         android:id="@+id/edit"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:layout_marginBottom="10dp"  
  12.         android:layout_marginLeft="20dp"  
  13.         android:layout_marginRight="20dp"  
  14.         android:layout_marginTop="10dp"  
  15.         android:hint="@string/edit_something_here" />  
  16.   
  17.     <Button  
  18.         android:id="@+id/more"  
  19.         android:layout_width="wrap_content"  
  20.         android:layout_height="wrap_content"  
  21.         android:layout_gravity="right"  
  22.         android:layout_marginRight="20dp"  
  23.         android:layout_marginBottom="10dp"  
  24.         android:text="More" />  
  25.       
  26.     <ViewStub   
  27.         android:id="@+id/view_stub"  
  28.         android:layout="@layout/profile_extra"  
  29.         android:layout_width="match_parent"  
  30.         android:layout_height="wrap_content"  
  31.         />  
  32.   
  33.     <include layout="@layout/ok_cancel_layout" />  
  34.   
  35. </LinearLayout>  
可以看到,这里我们新增了一个More Button,这个按钮就是用于去加载那些不常用的元素的,然后在Button的下面定义了一个ViewStub。在ViewStub控件中,我们先是通过id属性给它指定了一个唯一标识,又通过layout属性将profile_extra布局传入进来,接着给ViewStub指定了一个宽高。注意,虽然ViewStub是不占用任何空间的,但是每个布局都必须要指定layout_width和layout_height属性,否则运行就会报错。

接着修改ProfileActivity中的代码,在Activity中添加More Button的点击事件,并在点击事件中进行如下逻辑处理:

  1. private EditText editExtra1;  
  2. private EditText editExtra2;  
  3. private EditText editExtra3;  
  4.   
  5. public void onMoreClick() {  
  6.     ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub);  
  7.     if (viewStub != null) {  
  8.         View inflatedView = viewStub.inflate();  
  9.         editExtra1 = (EditText) inflatedView.findViewById(R.id.edit_extra1);  
  10.         editExtra2 = (EditText) inflatedView.findViewById(R.id.edit_extra2);  
  11.         editExtra3 = (EditText) inflatedView.findViewById(R.id.edit_extra3);  
  12.     }  
  13. }  
当点击More Button之后我们首先会调用findViewById()方法将ViewStub的实例获取到,拿到ViewStub的实例之后就很简单了,调用inflate()方法或者setVisibility(View.VISIBLE)都可以将隐藏的布局给加载出来,而加载的这个布局就是刚才在XML当中配置的profile_extra布局。

调用inflate()方法之后会将加载出来的布局进行返回,之后我们就可以对这个布局进行任意的操作了,再次隐藏显示,或者获取子元素的实例等。注意这里我对ViewStub的实例进行了一个非空判断,这是因为ViewStub在XML中定义的id只在一开始有效,一旦ViewStub中指定的布局加载之后,这个id也就失败了,那么此时findViewById()得到的值也会是空。

现在我们重新运行一下程序,界面如下图所示:


可以看到,界面上只有一个More按钮,ViewStub是完全不占用任何空间的。然后点击一下More按钮,新的界面如下所示:


没有问题,profile_extra.xml中定义的布局已经加载出来了,而且显示的位置也是在More按钮和OK按钮之间,正是ViewStub控件定义的位置,说明我们确实已经将ViewStub成功使用起来了。

另外需要提醒大家一点,ViewStub所加载的布局是不可以使用<merge>标签的,因此这有可能导致加载出来的布局存在着多余的嵌套结构,具体如何去取舍就要根据各自的实际情况来决定了,对于那些隐藏的布局文件结构相当复杂的情况,使用ViewStub还是一种相当不错的选择的,即使增加了一层无用的布局结构,仍然还是利大于弊。

经过四篇文章的学习,我们已经掌握了不少可以提高Android应用程序性能的技巧,这些技巧多数都是来自于Android Doc,我也是从中选取了一些感觉比较实用的部分,然后又加入了自己的理解呈现给大家。如果大家想要继续学习更多关于性能优化的技巧,可以到这个网址上阅读更多内容 http://developer.android.com/training/best-performance.html 。

好的,那么最佳性能实践系列的文章就到此结束,感谢大家有耐心看到最后。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值