您的位置:首页 > 移动开发 > Android开发

Android Layout 优化

2015-09-20 16:33 351 查看
  Layouts 是 Android app 中的很重要的一部分,它直接关系到用体验的问题 。如果 Layouts 没实现好,那么它会很容易导致内存紧,从而导致用户操作不流畅 。但是 Android SDK 有工具可以用来分析 layout performance 。在接下来的几章中,你将学会如何实现流畅的 layout,而内存占用能降到最少 。
  首先简要介绍一下各章节的内容
1. Optimizing Layout Hierarchies(优化 Layout 的层级结构)
    正如如果网页的内容太复杂混乱,那么它就会变慢一样,如果 Layout 的层级结构太复杂,那么应用的性能也会降低 。在这一节中,我们将会介绍如何使用 SDK 中的工具来检查 layout 的性能瓶颈 。
2. 利用 <include/> 重用 Layouts
    如果某部分 UI 在多个地方都要用到,那么你就应该重用它,而不是在每个需要的地方都写一遍,本章交会讲解如何利用 <include/> 来实现这种功能 。
3. 根据需要加载 views
    除了上面所说的包含 UI 外,可能你还想让某些 UI 在需要的时候才加载到界面上,不需要的时候消失掉 。本章将会讲解如何实现在需要的时候才加载 UI,以提高 layout 的初始化和加载性能 。
4. 使 ListView 更加流畅
    如果你的 ListView item 中包含一些重量级的内容,那么它的滑动性能将会受到影响 。本章将会讲解如何使 ListView 的操作更加流畅 。

一、Optimizing Layout Hierarchies

    也许大家都觉得,如果使用最基本的 layout 结构,那么 layouts 的性能一定不会有什么问题,但是这种观点是错误的 。因为你所添加到 app 上的每个 widget 和 layout 都需要 initialization(初始化)、layout(布局) 和 drawing(绘制)。如:使用多重嵌套的 LinearLayout 会导致生成的 view 的层级结构很深;还有,把几个使用了
layout_weight 属性的 LinearLayout 嵌套在一起,会导致每个 child 都需要 measured 两次,这样的系统开销是比较大的 。当各个 layout 需要单独 inflated 的时候,这两点的性能消耗会更加的明显(如当用到 ListView 或 GridView 的时候,各个 item 都需要单独 inflated,如果每个 item inflated 的开销都多一点点,那么整体上造成的性能消耗就非常大了)。
    本章主要讲如何使用 Hierarchy Viewer 和 Layoutopt 工具来检查(Inspect) layout 的性能及如何进行优化 。

1. Inspect Your Layout
    在 Android SDK tools 里面,有个叫做 Hierarchy Viewer 的工具,它可用于在 app 运行时分析 app 的 layout 。通过这个工具,就可以发现 layout 的瓶颈在哪里了 。
    Hierarchy Viewer 工具只有在与真实设备或 emulator 连接时才能使用,当连接好以后,它可以显示 layout tree 。在 layout tree 上,每一块中的三个点(指示灯)分别表示 Measure、Layout 和 Draw 的性能 。
    例如,下图 1 是 ListView 的一个 Item 的 layout,这个 layout 的左侧显示一张小图片,右侧有两个 text 。在多次 inflated 的时候,item layout 的 performance 是非常重要的 。



(图 1)

    hierarchyviewer 工具位于 <sdk>/tools/ 目录下,它会显示当前可用的
devices 列表及正在运行的 components 。点击 Load View Hierarchy 就可以查看到被选中的 component 的 layout hierarchy,下图 2 显示了上图
1 的 layout hierarchy 。



(图 2)



(图 3)

    从图 2 上可以看出,这个 listview 的 item 有三层结构,在对 text items 进行布局的时候有一些性能问题 。点击占用时间比较多的 items,可以看到它在每个阶段所消耗的时间,如上图 3 。这样就可以发现 在 measure、layout、render 的时候,哪个 item 消耗的时间最多,这样你也就知道要对哪些地方进行优化了 。从图 3 可以看到,对于这个
layout 来说,渲染完一个 list item 需要的时间为 Measure : 0.977ms、 Layout : 0.167ms、 Draw : 2.717ms 。

2. revise your layout
    很显然,前的 layout 之所以消耗很多时间,是因为我们使用了嵌套的 LinearLayout 。要提高性能,我们就要想办法使用 view hierarchy 变得更浅更宽,而不是更深更扁 。用 RelativeLayout 就可以做到这一点 。我们可以利用 RelativeLayout 把整个 hierarchy 就成两级,如下图 4 所示



(图 4)

    这时渲染一个 item 所需要的时间 Measure : 0.598ms、Layout : 0.110ms、Draw : 2.146ms 。看起来速度的提升并不是很明显,但是当要渲染很多 item 的时候,所有这些 item 所节省的时间就很明显了 。
    很多时候,使用和不使用 layout_weight 时,渲染时间会有明显的差别 。因为 layout_weight 会造成 measurement 的时间增多 。所以在使用 layout_weight 的时候,你需要考虑一下是否真的有必要 。

3. use Lint
    lint 工具可以检查 layout 文件中是否有可优化的 view hierarchy 。lint 工具已取代了 Layoutopt 工具,它提供了更多更强大的功能 。下面是 lint 工具的一些判定规则:
(1)使用 compound drawables(合成的 drawables):如对于一个包含一个 ImageView和一个 TextView 的 LinearLayout 来说,lint 工具会认为使用合成的 drawable 来代替这个 liearLayout 会更好一些 。
(2)Marge root frame(合并根元素):如果一个 layout 的根元素是 FrameLayout,而且这个 FrameLayout 没有提供 background 或 padding 等属性,那么 lint 会认为用 merge tag 来代替它会使性能有略微的提升 。
(3)删除 Useless leaf:如果一个 layout 没有子元素或 background 属性,那么就可以把它删掉,因为其实它是不可见的 。
(4)删除 Useless parent:如果一个 layout 只有一个孩子结点,而且它不是一个 ScrollView 或者它是一个 root layout,而且它没有 background,那么这个 parent 元素就可以删除掉了,直接把它的孩子结点放到更上一级的 parent 就行了,这样也可以提高性能 。
(5)不要出现层次太深的 Layouts:层次太深的 Layouts,它的性能往往是很差的,可以考虑多用 RelativeLayout 或 GridLayout 来提高性能,默认的最大层级数是 10 。

    Android Studio 已集成了 Lint 工具,每次编译程序的时候,Lint 工具都会自动运行 。你可以通过 File>Settings>Project Settings 来对 Android Studio 的 Lint 工具进行相关设置 。



二、Re-using Layouts with <include/>

    虽然 Android 提供了各种各样的 widgets,便于重用,但是有时你可能需要重用更大的 components,因为可能在多个 layout 中,你需要用到是一个 components 。这时 <include /> 和 <merge /> 标签就派上了用场 。这两个标签可用于把指定 components 嵌入到当前的 layout 中 。当需要重用很复杂的 layouts
时,它们就显得非常有用 。

1. 创建可重用的 Layout
    当你要重用某个 layout 的时候,你可以为它单独定义一个 XML 文件,如下面定义一个通用的 title bar,每个 activity 都可以用它(titlebar.xml)

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width=”match_parent”
    android:layout_height="wrap_content"
    android:background="@color/titlebar_bg">

    <ImageView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:src="@drawable/gafricalogo"/>
</FrameLayout>


2. 使用 <include /> 标签
    上面已创建好了通用的 title bar,在需要的时候,你就可以通过 <include /> 标签来把它包含进来,如下所示

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width=”match_parent”
    android:layout_height=”match_parent”
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width=”match_parent”
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp"/>

    ...

</LinearLayout>


    你也可以覆盖包含进来的 layout 的所有参数(所有的 android:layout_* 属性attributes),如下所示

<include android:id=”@+id/news_title”
         android:layout_width=”match_parent”
         android:layout_height=”match_parent”
         layout=”@layout/title”/>


    注意:如果你要覆盖参数,那么你必须覆盖 android:layout_height 和 android:layout_width 属性,否则覆盖无效 。

3. 使用 <merge /> 标签
    当你在一个 layout 中包含别的 layout 的时候,marge 标签可以用来消除 view hierarchy 中多余的 view groups 。例如:如果你有一个 layoutA ,它是个是 vertical 属性的 LinearLayout,在 layoutA 中有两个连续的 views,在其它的很多个 layout 中,都需要用到 layoutA 。因为 layoutA
中有两个 views,所以它需要一个 rootview 。这样,当 layoutA 被 include 到其它的 layout 中的时候,就会多了一重嵌套,这会导致 UI 性能降低 。为了减少这种不必要的嵌套,你可以用 <merge> 元素来做 layoutA 的 root view,如下

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

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/add"/>

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/delete"/>

</merge>


    这样,当你把 layoutA include 到其它的 layout 的时候,系统就会忽略掉 <merge> 元素,直接把它里面的 button 包含到当前的 layout 中,并以此替换掉 <include /> 标签 。

三、Loading Views On Demand

    有时候,有些复杂的 views 不经常用到,它们可能是 item 的 details 或进度条等,它们只在特定的时候才需要显示出来,所以你可以只在需要的时候才加载它们,这样提升 layout 的性能 。

1. 定义  ViewStub
    ViewStub 是一个轻量级的 view,它没有 dimension,而且在 layout 的过程当中,它不会被 draw 出来 。所以 inflate 或把它从 view hierarchy 中移除的开销是很小的 。每个 ViewStub 都需要有 android:layout* 属性 。
    下面这个 ViewStub 用于显示一个进度条,在没有加载新的 items 的时候,它应该处于不可见状态,如下

<ViewStub
    android:id="@+id/stub_import"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"/>


2. 加载 ViewStub Layout
    当你想加载 ViewStub 绑定的 layout 的时候,你可以调用 
setVisibility(View.VISIBLE)
 或 
inflate() 
中的任意一个,这样对应的
layout 就会显示出来,如下

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel =((ViewStub) findViewById(R.id.stub_import)).inflate();


    一旦 inflate 操作完成,inflate() 方法就会返回被 inflated 的 View 。一旦 visible 或 inflated(即当处于可见状态或被 inflated 后),ViewStub 元素就不再属于 view hierarchy 的一部分了,此时它已经被 inflated 进来的 layout 替换掉了 。而此时被 inflated 进来的 layout
的 rootview 的 id 就是在 ViewStub 中指定的 inflatedId 。所以 ViewStub 中的 android:id 属性在 ViewStub处于可见状态或被 inflated 后就不存在了 。
    注意:ViewStub 的一个缺点是它不支持在它所绑定的 layout 中使用 <merge /> 标签 。

四、Making ListView Scrolling Smooth

    使用 ListView 的滑动操作流畅的关键是要想办法降低 UI 线程(即主线程)的负担,所以当需要访问 disk、network 或 SQL 的时候,应该在独立的线程中进行 。你可以开启 StrictMode 来检测 app 和性能 。

1. 使用 Background 线程
    使用 background 线程可以减轻主线程的负担,这样主线程可更专注于 UI 的绘制渲染 。在很多情况下,你可以用 AsyncTask 来进行一些耗时操作,因为 AsyncTask 会开启另外的线程来完成它的任务 。AsyncTask 会自动地把所有的 execute() 请求压入任务队列,然后顺序地执行它们 。所以你不需要担心如何去建立维护自己的线程池 。
    在下面这段示例代码中,我们用一个 AsnycTask 来在后台加载一张图片,当加载完成时,才让 UI 线程进行绘制操作 。而且在加载图片的过程中,它会显示一个滚动条,代表正在加载中

// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<ViewHolder,Void,Bitmap>(){
    private ViewHolder v;

    @Override
    protected Bitmap doInBackground(ViewHolder...params){
        v = params[0];
        return mFakeImageLoader.getImage();
    }

    @Override
    protected void onPostExecute(Bitmap result){
        super.onPostExecute(result);
        if(v.position == position){
            // If this item hasn't been recycled already, hide the
            // progress and set and show the image
            v.progress.setVisibility(View.GONE);
            v.icon.setVisibility(View.VISIBLE);
            v.icon.setImageBitmap(result);
        }
    }
}.execute(holder);


    从 Android 3.0(API level 11)开始,AsyncTask 还有另外的一个特性:可以在用多个处理器来完成同一任务,只要你启动任务的时候,调用 
executeOnExecutor()
 而不是调用 execute() 方法就行,这样设备上有多少个处理器,AsyncTask
就可以利用多少个处理器来执行它的任务 。

2. 用 View Holder 来保留 View Objects
    在滑动 ListView 的过程中,findViewById() 方法可能会被频繁地调用,该方法的频繁调用也会造成性能降低 。而避免频繁调用 findViewById() 的方法就是使用 view holder 。一个 ViewHolder object 可以存储 layout 中所有 view 的 tag field,这样系统就不用频繁地查询它们了 。下面的代码片段是一个示例的
ViewHolder 类:

static class ViewHolder{
  TextView text;
  TextView timestamp;
  ImageView icon;
  ProgressBar progress;
  int position;
}


然后就可以填充这个 ViewHolder,并把它存放在 layout 中,如下

ViewHolder holder =new ViewHolder();
holder.icon =(ImageView) convertView.findViewById(R.id.listitem_image);
holder.text =(TextView) convertView.findViewById(R.id.listitem_text);
holder.timestamp =(TextView) convertView.findViewById(R.id.listitem_timestamp);
holder.progress =(ProgressBar) convertView.findViewById(R.id.progress_spinner);
convertView.setTag(holder);


现在,在访问每个 view 的时候,就不需要再重新查询它的 field 了(即不用反复地调用 findViewById() 了) 。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息