Android布局性能优化指南

Android布局是应用的重要组成部分,它直接影响到用户的体验。如果布局不合理则会导致内存占用过多且UI卡顿。Android SDK提供了一些工具可以帮助我们快速定位到影响性能的布局问题,一般可从以下几个方面来进行布局优化。

优化布局层次结构

众所周知,复杂的网页加载速度很慢,Android应用也一样,复杂的布局结构也将引起性能问题。下面来说明如何使用工具来检查布局并发现性能瓶颈。

我们知道,应用中的每个组件及布局都需要初始化、测量、绘制等流程,例如使用了嵌套的LinearLayout将会导致更深的View层次,一旦嵌套的LinearLayout使用了layout_weight属性则将导致更长的加载时间,因为每个子控件将被测量两次。这种影响在ListView或GridView中将会更加明显,因为这两个控件的每一个Item都会重用一套布局。

我们将使用Hierarchy Viewer来检查并优化布局。

检查布局

Hierarchy Viewer是Android SDK tools提供的一个工具,它位于sdk/tools/目录下,可以用来分析布局并发现其中的性能瓶颈。需要注意到是使用Hierarchy Viewer时需要保持App正在运行(使用模拟器或连接真机,真机需要支持调试模式),并保持当前应用的进程正常连接。下面举一个例子:

这里写图片描述

上图是一个ListView的行布局结构,最外层是一个水平方向的LinearLayout,其内部左侧是一个ImageView用来显示图片,右侧是一个垂直方向的LinearLayout,该LinearLayout又包含上下两个TextView用来显示文本。这种布局结构在我们日常开发中非常常见,接下来我们检测一下它的布局性能。

打开Hierarchy Viewer工具后,点击“Load View Hierarchy”,结果如下图所示:

这里写图片描述

从上图可以看到,这是一个深度为3层的布局结构,点击每一块布局则会显示其测量、布局、绘制3个过程的时间消耗,如下图所示:

这里写图片描述

它说明使用该布局完全渲染一条列表项的具体耗时为:

  • 测量:0.977ms
  • 布局:0.167ms
  • 绘制:2.717ms

这样开发者就能够清楚地发现哪里是布局的性能瓶颈,接下来就可以针对性地进行优化了。

修改布局

上述布局由于使用了嵌套的LinearLayout才导致性能下降,因此提高性能的办法就是减少布局的层次嵌套,使之扁平化。我们将最外层的LinearLayout替换成RelativeLayout,使用相对布局,可以将原来3层的布局减少为两层。再次使用Hierarchy Viewer查看,结果如下:

这里写图片描述

现在每一行的布局渲染时间消耗为:

  • 测量:0.598ms
  • 布局:0.110ms
  • 绘制:2.146ms

可以看到,性能已经得到了细微的提升,千万别小看这微小的提升,这个布局在ListView中可是会被多次调用的,因此整体性能提升是很可观的。

LinearLayout使用了layout_weight会降低测量的速度,因此在使用权重时务必谨慎,能不用则不用。

使用Lint

我们还可以使用Lint工具来发现布局中可优化的地方,Lint已经内置到Android Studio中,使用非常方便。Lint有以下常用规则:

  • 使用复合Drawable——如果一个LinearLayout包含一个ImageView和一个TextView则使用复合Drawable的方式会更加高效。
  • 合并根布局——如果FrameLayout为根布局且没有背景或内边距等属性,则可以使用marge标签来合并跟布局以提高效率。
  • 无用的叶节点布局——一个布局如果没有子布局且无背景属性,则可以移除它,因为它不会显示出来,移除之后使得布局层次更加扁平且高效。
  • 无用的父布局——如果一个布局(除ScrollView、根布局之外)没有背景等属性,且它的子布局也无兄弟布局,就可以将它本身移除,将其子布局直接移出来。
  • 布局层次过深——布局层次嵌套过多严重影响性能,可考虑使用RelativeLayout或GridLaout来提升性能,建议布局层次深度不要超过10层。

Lint能够自动帮助我们修复一些问题、可以提供修改建议或者跳转到出问题的代码部分,建议大家好好利用Lint这个工具。

使用include重用布局

如果某一种特定的布局结构在应用中出现多次,则可以使用include来重用该布局,提高效率。

定义重用布局

加入我们想重用某个布局,可以单独为它创建一个xml布局,例如,定义一个TitleBar(titlebar.xml),每一个Activity都可以重用它。titlebar.xml内容如下:

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

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

上述定义中的tools:showIn属性指定了一个父布局来include该重用布局,此属性将会在编译时移除,它只是为了方便在开发时预览布局效果。

使用include标签

在需要重用布局的地方使用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>

也可以为include标签下的布局重写android:layout_*等属性,如:

<include android:id="@+id/news_title"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         layout="@layout/title"/>
使用merge标签

merge标签可以帮助我们消除冗余布局,例如最外层布局是一个垂直方向的LinearLayout,它内部是一个可重用的布局,而这个可重用布局也是一个垂直方向的LinearLayout包含上下两个Button。那么导致的结果是:一个垂直方向的LinearLayout包含另一个垂直方向的LinearLayout,这种冗余嵌套必然延迟UI的加载效率。

对于上述情况,可以使用merge来消除冗余,修改如下:

<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>

按需加载视图

有时候,一些复杂的View只是偶尔才需要显示,那么就可以在需要时再加载它们,这样减小内存占用并提高渲染速度,通常的做法就是使用ViewStub。

定义ViewStub

ViewStub是一个轻量级的view,它不会在布局加载时进行绘制与展示,因此它几乎没有性能开销。每一个ViewStub需要一个android:layout属性来指定要加载的布局。

以下ViewStub示例为一个透明的进度条覆盖层,它只是在新内容加载时才会显示。

<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" />
加载ViewStub布局

当你需要加载ViewStub指定的布局时,有两种方法:

  • 通过调用setVisibility(View.VISIBLE)将其设为可见
  • 调用inflate()
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

需要注意的是inflate()方法完成后会返回ViewStub指定的布局,因此不再需要通过findViewById()来得到此布局。

ViewStub一旦被inflate或设为可见,ViewStub元素将不再作为View层次结构的一部分。原来的ViewStub将会被它指定的布局所替代,而这个布局的id就是在ViewStub中指定的android:inflatedId属性的值。而原来的ViewStub的id,即Android:id也将无效。

ViewStub的一个缺点是它指定的布局不支持标签的使用。

使ListView流畅地滑动

保证ListView流畅滑动的关键是应用的主线程没有耗时的事务处理,一些耗时操作如磁盘访问、网络访问、或数据库访问需要使用后台线程。

使用后台线程

使用后台线程可以减轻UI线程的负担,这样UI线程可以专注于UI绘制,保证流程的用户体验。AsyncTask提供了一种简单的后台线程的调用方法,如下示例为使用AsyncTask下载图片,图片下载完成后会显示到视图组件上:

// 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)起,可以通过使用executeOnExecutor()在后台并行处理多个请求。

使用ViewHolder

在ListView滑动时,会频繁调用findViewById(),这将降低性能。即使Adapter返回一个重用的布局,仍然需要找到对应元素并更新它们。一种可以替代频繁调用findViewById()的方式是使用ViewHolder。

ViewHolder对象存储了每一个view组件,最终通过setTag方法保存到Layout的tag字段中,这样就能够快速访问每个组件。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);

然后就可以轻松地访问每个视图,而不需要查找,从而节省宝贵的处理时间。

参考文献:Improving Layout Performance

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值