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);
然后就可以轻松地访问每个视图,而不需要查找,从而节省宝贵的处理时间。