Android应用性能优化最佳实践.2.3 布局优化

2.3 布局优化


布局是否合理主要影响的是页面测量时间的多少,我们知道一个页面的显示测量和绘制过程都是通过递归来完成的,多叉树遍历的时间与树的高度h相关,其时间复杂度为O(h),如果层级太深,每增加一层则会增加更多的页面显示时间。

任何时候View中的绘制内容发生变化时,都需要重新创建DisplayList、渲染DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于View的复杂程度、View的状态变化以及渲染管道的执行性能。例如,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置,则会触发HierarchView重新计算其他View的位置。如果布局很复杂,就很容易导致严重的性能问题。

在优化前首先讲解两个布局优化的常用工具。

2.3.1 常用布局优化工具

1.?Hierarchy Viewer

Hierarchy Viewer是Android SDK自带的一款可视化调试工具,用来检查Layout嵌套及绘制时间,以可视化的布局角度直观获取Layout布局设计和各种属性信息,开发者在调试和布局UI界面时可以很方便地使用,提高用户的开发效率。

出于安全考虑,Hierarchy Viewer只能连接Android开发版手机或模拟器。

在应用程序DEBUG模式中,无法启动Hierarchy Viewer。

接下来一步步介绍如何使用Hierarchy Viewer。

Step1:构造页面

先构造一个简单的页面LayoutPerActivity,该页面如图2-20所示,然后启动应用,进入这个页面。

 

图2-20 页面显示

Step2:打开Hierarchy View

Eclipse和Android Studio都带有Hierarchy View工具,下面介绍在这两款IDE上打开Hierarchy View的方法。

Android Studio

在Android Studio上可以直接在快捷工具栏打开Android Device Monitor,如图2-21所示。Android Device Monitor上就有Hierarchy View视图,可直接查看。

或者选择Tools->Android->Android Device Monitor菜单,直接打开Hierarchy View。

Eclipse

在Eclipse的ADT Android插件中,不能直接启动Hierachy Viewer,可以从Android SDK工具包中,通过命令行的方式启动,在Android SDK下的tools目录下,在命令行方式下运行hierachyviewer。

使用AS打开后的整体窗口如图2-22所示。

 

图2-22 Hierarchy View整体窗口

可以看到4个窗口,各个窗口的功能如下:

Windows:显示当前设备信息,以及当前设备的所有页面列表。

View Properties:当前选中View的属性。

TreeView:把Activity中所有控件(View)的层次结构从左到右显示出来,其中最右边部分是最底层的控件(View)。

Tree Overview:全局概览,以缩略图的方式显示整个应用中各控件的层次关系,并且框出TreeView窗口中显示部分在全局中的位置,如果一个界面中的控件和层级比较多,可以通过鼠标移动这个显示区域移动。

Layout View:整体Layout布局图,以手机屏幕上真实位置呈现出来,在TreeView中选中某一个控件时,会在Layout View用红色的框标注。

Step3:使用Hierarchy Viewer查看层级和耗时

查看层级图:在Windows窗口页,选择需要查看的组件,双击或单击Load View Hierarchy按钮即可打开。双击后在Tree View界面中,从左到右把所有控件层级图显示出来,这样就可以看到整体界面的层级深度。

查看某个View的耗时:在快捷键工具栏中单击Obtain layout times for tree rooted at selected node按钮,如图2-23所示。

 

图2-23 查看单个View的耗时

这时可以看到Tree View中的页面增加了属性,单击一个控件,可以看到这个View的耗时情况。

根据图2-24从上往下看,1 view表示这个控件是这个树下的最后一个控件,即表示是它本身,下面的时间表示Measure、Layout以及Draw三个阶段的耗时。最后一个框有不同色的三个指示灯,分别对应当前控件在测量、布局以及画视图三个阶段,颜色表示这个控件占用的时间百分比,如果是绿色的,表示该控件在该阶段比其他50%的控件的速度要快,黄色表示比其他50%的控件的速度要慢,红色表示该控件在该阶段的处理速度是最慢的,就需要注意了。

到这里我们就知道如何使用Hierarchy View来分析一个页面的层级和耗时,并且使用这个工具,用户可以很方便地查看和调试应用中的UI界面,分析其性能,建议开发者在开发阶段也使用这款工具。但一个应用的界面非常多,如果一个个这样分析的话效率非常低,所以再介绍另外一个工具Lint,用于检查所有页面的层级,并把深度高于N(自定义)的界面输出,然后通过Hierarchy View工具来仔细分析。

2.?布局层级检查

Android Lint是Android SDK Tools 16(ADT 16)之后引入的代码检查工具,通过代码静态检查,可以发现潜在的代码问题,并给出优化建议。Android-Lint检查工具使用的方式有以下两种:

命令行使用脚本执行。

在IDE中使用视图化工具。

Lint的检查结果分为6类,如图2-25所示。

 

图2-25 Android Lint示意图

Correctness(正确性)

Security(安全性)

Performance(性能)

Usability(可用性)

Accessibility(可达性)

国际化

问题的严重程度(severity)从高到低依次是:

Fatal

Error

Warning

Information

Ignore

扫描规则和缺陷级别可以在File→Settings→Inspections→Android Lint中配置,Lint的功能非常强大,强烈建议开发者深入学习使用方法,这里只讲解如何使用Android Lint来发现XML布局检查。使用Lint扫描前,先配置需要检查的项目,只需要检查Layout层级深度。先进入File→Settings→Inspections→Android Lint。

如图2-26所示,这里的配置只扫描Layout的层级和View的个数,如下所示:

TooDeepLayout:表示布局太深,默认层级超过10层会提示该问题,可以自定义环境变量ANDROID_LINT_MAX_DEPTH来修改。布局深度增加会导致内存消耗也随之增加,因此布局尽可能浅而宽。

TooManyViews:表示控件太多,默认超过80个控件会提示该问题。

在Android Studio中启动Lint,从菜单栏选择Analyze→Inspect Code,进去后可以指定扫描的范围,可以是整个工程,也可以是一个Module或单独的文件。启动扫描,扫描结果如图2-27所示。

 

图2-26 设置Lint规则

可以很清楚地显示哪个layout有问题,进而打开对应文件并修改。介绍完两个工具,接下来我们从多个方面来优化UI,让应用使用更流畅。

2.3.2 布局优化方法

在Android应用开发中,常用的布局方式主要有LinearLayout、RelativeLayout、FrameLayout等,通过这些布局可以实现各种各样的界面。我们需要知道如何高效地使用这些布局方式来组织UI控件,布局的好坏影响到绘制的时间,本节将通过减少Layout层级,减少测量、绘制时间,提高复用性三个方面来优化布局,优化的目的就是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性节省开发和维护成本。

1.?减少层级

层级越少,测试和绘制的时间就越短,通常减少层级有以下两个常用方案:

合理使用RelativeLayout和LinearLayout。

合理使用Merge。

(1)RelativeLayout与LinearLayout

使用LinearLayout布局的具体方法,见代码清单2-1。

代码清单2-1 使用LinearLayout布局

<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical">

    <LinearLayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:orientation="horizontal">

        <TextView

            android:id="@+id/layout_per_txt_1"

            android:layout_width="wrap_content"

            android:layout_height="100dp"

            android:text="TITLE"

            android:textSize="20sp" />

        <LinearLayout

            android:layout_width="match_parent"

            android:layout_height="100dp"

            android:orientation="vertical">

            <TextView

                android:id="@+id/layout_per_txt_2"

                android:layout_width="fill_parent"

                android:layout_height="50dp"

                android:text="Des"/>

            <ImageView

                android:id="@+id/imageView_2"

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:background="@mipmap/ic_launcher" />

        </LinearLayout>

    </LinearLayout>

</LinearLayout>

这是一个简单的布局页面,在根视图上嵌套了两个LinearLayout,并且有两层显示界面,如图2-28所示。

使用2.3.1节介绍的方法通过Hierarchy View来查看下层级情况,如图2-29所示。

 

图2-29 Hierarchy View检查结果

由图2-29可以看到一共有7级,使用RelativeLayout进行优化,达到相同的布局效果,并且RelativeLayout允许子元素指定它们相对于其他元素或父元素的位置,有最大*度的布局属性,而且布局层次最浅,占用内存最少。修改见代码清单2-2。

代码清单2-2 使用RelativeLayou布局

<RelativeLayout xmlns:android="http:// schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical">

    <TextView

        android:id="@+id/layout_per_txt_1"

        android:layout_width="100dp"

        android:layout_height="100dp"

        android:background="@color/yellow"

        android:text="TITLE"

        android:textColor="#FFFF0000"

        android:textSize="20sp" />

    <TextView

        android:id="@+id/layout_per_txt_2"

        android:layout_width="match_parent"

        android:layout_height="50dp"

        android:layout_toEndOf="@id/layout_per_txt_1"

        android:layout_toRightOf="@id/layout_per_txt_1"

        android:background="@color/blue"

        android:text="Des"/>

    <ImageView

        android:id="@+id/imageView_2"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_below="@id/layout_per_txt_2"

        android:layout_toEndOf="@id/layout_per_txt_1"

        android:layout_toRightOf="@id/layout_per_txt_1"

        android:background="@mipmap/ic_launcher" />

</RelativeLayout>

这样就可以减少两个层级,用一个RelativeLayout就可以达到显示的效果,再使用Hierarchy View来查看层级,可以看到减少到5层,如图2-30所示。

 

图2-30 优化后的Hierarchy View检查结果

但ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中子View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向互相依赖,就需要横向、纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高,在布局上RelativeLayout不如LinearLayout快。

但是如果布局本身层次太深,还是推荐用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但在体验上影响不会特别大,如果优化掉两层仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。

根据以上分析,可以总结出以下几点布局原则:

尽量使用RelativeLayout和LinearLayout。

在布局层级相同的情况下,使用LinearLayout。

用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。

由于Android的碎片化程度很高,市面上的屏幕尺寸也是各式各样,使用RelativeLayout能使构建的布局适应性更强,构建出来的UI布局对多屏幕的适配效果更好,通过指定UI控件间的相对位置,使不同屏幕上布局的表现基本保持一致。当然,也不是所有情况下都得使用相对布局,根据具体情况选择和搭配使用其他布局方式来实现最优布局。

(2)Merge的使用

从名字上就可以看出,Merge就是合并的意思。使用它可以有效优化某些符合条件的多余的层级。使用Merge的场合主要有以下两处:

在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout。

在Activity中整体布局,根元素需要是FrameLayout。

我们仍以前面的布局为例,在页面增加一个自定义控件TopBar,故在代码清单2-2布局的基础上增加如下代码:

<RelativeLayout xmlns:android="http:// schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical">

    <com.ycl.androidtech.ui.TopBar

        android:id="@+id/lay_out_topbar"

        android:layout_width="fill_parent"

        android:layout_height="50dp"/>

    ……………

</RelativeLayout >

其中TopBar的XML布局如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"

    android:orientation="horizontal" android:layout_width="match_parent"

    android:layout_height="@dimen/topbar_height">

    <ImageView

        android:layout_gravity="center"

        android:id="@+id/backImg"

        android:layout_width="60dp"

        android:layout_height="44dp"

        android:background="@drawable/img_top_back"

        android:focusable="true" />

    <TextView

        android:id="@+id/titleTextView"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_centerVertical="true"

        android:text="标题"/>

</LinearLayout>

显示结果如图2-31所示。这种布局在一些列表的Item中非常常见,而且列表中Item本身的层级比较深,因此优化显得更有意义。

我们使用HierarchyView查看增加TopBar后的布局层级,如图2-32所示。可以看到,就是这么简单的一个布局,却把层级增加了两级,从图2-32中很明显地看出TopBar后一层的LinearLayout是多余的,这时可以使用Merge把这一层消除。

 

图2-32 增加TopBar后的布局层级

使用Merge来优化布局,使用Merge标签替换LinearLayout后,原来的LinearLayout属性也没有用了,修改后的代码如代码清单2-3。

代码清单 2-3

<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http:// schemas.android.com/apk/res/android"

    android:orientation="horizontal" android:layout_width="match_parent"

    android:layout_height="@dimen/topbar_height">

    <ImageView

        android:layout_gravity="center"

        android:id="@+id/backImg"

        android:layout_width="60dp"

        android:layout_height="44dp"

        android:background="@drawable/img_top_back"  />

    <TextView

        android:layout_gravity="center"

        android:id="@+id/titleTextView"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_centerVertical="true"

        android:singleLine="true"

        android:text="标题"

        android:textSize="18sp" />

</merge>

运行后再使用Hierarchy View查看当前层级,如图2-33所示。

 

图2-33 合并后的布局层级

这样就把多余的LinearLayout消除了,原理是在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中,这样就保证了不会引入额外的层级。

如果Merge代替的布局元素为LinearLayout,在自定义布局代码中将LinearLayout的属性添加到引用上,如垂直或水平布局、背景色等。

但Merge不是所有地方都可以任意使用,有以下几点要求:

Merge只能用在布局XML文件的根元素。

使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inf?late(int, ViewGroup, boolean))。

不能在ViewStub中使用Merge标签。原因就是ViewStub的inf?late方法中根本没有attachToRoot的设置。

这一节讲了如何减少层级,那么在Android系统中,多少层才是合理的呢?当然是越少越好,但从Lint检查的配置上看,超过10层才会报警,实际上在开发时,随着产品设计的丰富和多样性,很容易超过10层,根据实际开发过程中超过15层就要重视并准备做优化,20层就必须修改了。在实在没有办法优化的情况下,需要把复杂的层级用自绘控件来实现,自绘控件中的图层层级再多,在布局上也只是一层,但这样也会带来过度绘制的问题,在后一章节中会重点介绍这个问题的优化方案。

在Activiy的总布局中使用Merge,但又想设置整体的属性(布局方式或背景色),可以不使用setContentView方法加载Layout,而使用(id/content)将FrameLayout取出来,在代码中手动加载布局,但如果层级压力不大(小于10级),则没有必要,因为这样代码的维护性较差。

2.?提高显示速度

我们在开发的过程中会碰到这样的场景或者显示逻辑:某个布局当中的子布局非常多,但并不是所有元素都同时显示出来,而是二选一或者N选一,打开这个界面根据不同的场景和属性显示不同的Layout。例如:一个页面对不同的用户(未登录、普通用户、会员)来说,显示的布局不同。或者,有些用户喜欢对不同的元素使用INVISIBLE或者GONE隐藏,通过设计元素的visable属性来控制,这样虽然达到了隐藏的目的,但效率非常低,原因是即使将元素隐藏,它们仍在布局中,仍会测试和解析这些布局。Android提供了ViewStub控件来解决这个场景。

ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inf?late()时,ViewStub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。这样,就可以使用ViewStub来设置是否显示某个布局。

代码清单2-4是两个ViewStub通过不同的初始化来加载两个不同的布局,以满足用户的需求。

代码清单2-4 使用ViewStub

<?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="vertical">

    <include

        android:id="@+id/topBar"

        layout="@layout/common_top_bar"

        android:layout_width="match_parent"

        android:layout_height="@dimen/topbar_height" />

    <ViewStub

        android:id="@+id/viewstub_text"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout="@layout/viewstub_text_layout1"/>

    <ViewStub

        android:id="@+id/viewstub_image"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout="@layout/layout_bitmap_show"/>

</LinearLayout>

在调用时,根据需求切换不同的Layout,这样可以提高页面初始化的速度,使用代码如下:

View view = inflater.inflate(R.layout.fm_xml_show, container, false);

if (changeView) {

    ViewStub stub = (ViewStub) view.findViewById(R.id.viewstub_text);

    stub.inflate();

    changeView = false;

} else {

    ViewStub stub = (ViewStub) view.findViewById(R.id.viewstub_image);

    stub.inflate();

    changeView = true;

}

ViewStub显示有两种方式,上面代码使用的是inf?late方法,也可以直接使用ViewStub.setVisibiltity(View.Visible)方法。

使用ViewStub时需要注意以下几点:

ViewStub只能加载一次,之后ViewStub对象会被置为空。换句话说,某个被ViewStub指定的布局被加载后,就不能再通过ViewStub来控制它了。所以它不适用于需要按需显示隐藏的情况。

ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的View,还是使用visibility属性。

VIewStub中不能嵌套Merge标签。

不过这些限制都无伤大雅,我们还是能够用ViewStub来做很多事情,ViewStub的主要使用场景如下:

在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。

想要控制显示与隐藏的是一个布局文件,而非某个View。

因为ViewStub只能Inf?late一次,之后会被置空,无法继续使用ViewStub来控制布局。所以当需要在运行时不止一次显示和隐藏某个布局时,使用ViewStub是无法实现的。这时只能使用View的可见性来控制。

3.?布局复用

我们在开发应用时还会碰到另一个常见的场景,就是一个相同的布局在很多页面(Activity或Fragment)会用到,如果给这些页面的布局文件都统一加上相同的布局代码,维护起来就很麻烦,可读性也差,一旦需要修改,很容易有漏掉的地方,Android的布局复用可以通过<include>标签来实现,就像提取代码公用部分一样,在编写Android布局文件时,也可以将相同的部分提取出来,在使用时,用<include>添加进去。例如:

<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"

    ……

    android:orientation="vertical">

    <include

        android:id="@+id/topBar"

        layout="@layout/common_top_bar"

        android:layout_width="match_parent"

        android:layout_height="@dimen/topbar_height" />

</ LinearLayout >

例如,在大部分应用中,基本上所有的应用都会带有头部栏(TopBar),主要是显示标题和返回键功能,这样只需要维护一份代码,就可以修改所有的显示效果。这个示例的TopBar布局XML如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http:// schemas.android.com/apk/res/android"

    android:orientation="horizontal" android:layout_width="match_parent"

    android:background="@color/black"

    android:layout_height="@dimen/topbar_height">

    <ImageView

        android:layout_gravity="center"

        android:id="@+id/backImg"

        android:layout_width="60dp"

        android:layout_height="44dp"

        android:background="@drawable/img_top_back"

        android:focusable="true" />

    <TextView

        android:layout_gravity="center"

        android:id="@+id/titleTextView"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:ellipsize="end"

        android:layout_centerVertical="true"

        android:gravity="center"

        android:singleLine="true"

        android:textColor="#FFFFFFFF"

        android:text="标题"

        android:textSize="18sp" />

</LinearLayout>

类似于TopBar的这类常用控件,包括菜单,可以把具体实现抽象到页面的基类(BaseActivity)中,这样布局和具体的实现都收归到一个地方,方便维护。

提高布局效率的方法总体来说就是减少层级,提高绘制速度和布局复用。影响布局效率主要有以下几点:

布局的层级越少,加载速度越快。

减少同一层级控件的数量,加载速度会变快。

一个控件的属性越少,解析越快。

根据本节的分析,对优化的总结如下:

尽量多使用RelativeLayout或LinearLayout,不要使用绝对布局AbsoluteLayout。

将可复用的组件抽取出来并通过< include />标签使用。

使用< ViewStub />标签加载一些不常用的布局。

使用< merge />标签减少布局的嵌套层次。

尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。

删除控件中的无用属性。

上一篇:Machine Learning与Deep Learning


下一篇:SpringBoot 如何进行限流