改进布局性能
布局是Android应用核心组成部分,它直接影响到用户体验,如果你没有很好的实现,你实现的布局有可能导致应用内存吃紧,从而导致UI渲染变慢,Android SDK提供了一些工具来帮助我们找出我们的布局中存在的性能问题,学完以下几个知识点,你将有能力让你的应用运行流畅,占用很少的内存
优化布局结构
大家有个共同的误区就是认为使用基本的布局结构能够带来最高效的布局效果,然而,任何一个添加进应用的控件和布局都需要经过初始化、布绘和渲染,例如,使用嵌套的LinearLayout会导致过深的视图层次,而且,嵌套几个LinearLayout的话,尤其当你使用layout_weight 属性时是极度损耗性能的,因为LinearLayout的每一个子控件都要测量两次。这一点非常重要当你的布局需要反复渲染显示时,比如在你使用GirdView或ListView的时候。
详细检查你的布局
Android SDK工具集包含了一个叫Hierarchy Viewer 的工具, 来帮助分析当我们的应用在运行的时候,布局的渲染性能,使用Hierarchy Viewer 工具能帮助发现我们编写的布局文件的性能瓶颈
Hierarchy Viewer 能够让我们通过选择一个正在手机或模拟器上面运行的进程来工作,然后显示进程中正在后用户交互的界面的控件树,每个块的交通灯表示它的测量,布局和绘图性能,帮助你检查出潜在的问题。
例如,图-1显示了在 ListView 中一项的布局,一张小图片和两行文本,这种需要多次渲染的布局优化的性能优势将成倍增加。
Hierarchy Viewer 能够让我们通过选择一个正在手机或模拟器上面运行的进程来工作,然后显示进程中正在后用户交互的界面的控件树,每个块的交通灯表示它的测量,布局和绘图性能,帮助你检查出潜在的问题。
例如,图-1显示了在 ListView 中一项的布局,一张小图片和两行文本,这种需要多次渲染的布局优化的性能优势将成倍增加。
hierarchyviewer 工具可以再<sdk>/tools/目录下找到和使用,当你打开它时,它会显示当前你电脑已经连接的设备和它们正在运行的组件, 点击 Load View Hierarchy 按钮可以显示所选组件的布局的层次
Figure 2. Layout hierarchy for the layout in figure 1, using nested instances of
LinearLayout
.
Figure 3.
Clicking a hierarchy node shows its performance times.
通过这个我们可以知道,每个层次,每个控件测量、布局和绘制所花费的时间,从中可以找到我们需要优化的点
修复你的布局
使用嵌套的布局文件会使布局渲染性能下降,使用单一扁平的布局可能会改善性能,使布局浅宽比窄深更好,使用RelativeLayout 相对布局可以使布局层次更浅
Figure 4. Layout hierarchy for the layout in figure 1, using RelativeLayout
.
Now rendering a list item takes:
- Measure: 0.598ms
- Layout: 0.110ms
- Draw: 2.146ms
使用RelativeLayout相对 来说会使性能改善点,但你想想当你需要显示成千上万个这样的布局时,就可以提高很多性能了
这个时间差大部分是由于在LinearLayout中的设计使用layout_weight,这个会降慢测量的速度。我们应该适当的使用每一个布局,当你在使用LinearLayout时,需要想想是否真的有必要
在实际使用中发现
hierarchy 并不能工作于真机上,好像只能工作于模拟器上,不知道是不是我的机子问题
使用Lint
我们可以在布局文件上使用Lint工具来寻找视图可能存在的层次优化点,Lint已经替代了Layoutopt 工具并且具有很多更棒的功能,Lint的一些规则:
使用复合画板 - 在一个包含ImgeView和TextView的LinearLayout中使用复合画板会更高效
如果一个布局下面有子控件但没有兄弟控件,且不是一个根布局,不是 ScrollView, 没有背景,那么可以移除它
如果一个布局没有包含子控件,没有背景,那么它将不会在界面上显示,我们可以移除它
尽量使用平面布局 RelativeLayout or GridLayout
如果以个FrameLayout 是根布局,且没有背景,没有填充,可以用merge标签来取代它
我们可以很方便的在eclipse中使用Lint,可以对一个项目、一个布局文件使用Lint来分析那些地方需要优化,来获得Lint给我们的建议
使用复合画板 - 在一个包含ImgeView和TextView的LinearLayout中使用复合画板会更高效
如果一个布局下面有子控件但没有兄弟控件,且不是一个根布局,不是 ScrollView, 没有背景,那么可以移除它
如果一个布局没有包含子控件,没有背景,那么它将不会在界面上显示,我们可以移除它
尽量使用平面布局 RelativeLayout or GridLayout
如果以个FrameLayout 是根布局,且没有背景,没有填充,可以用merge标签来取代它
我们可以很方便的在eclipse中使用Lint,可以对一个项目、一个布局文件使用Lint来分析那些地方需要优化,来获得Lint给我们的建议
用<include/>复用布局
尽管Android提供了多种多样的小部件来构建小而可复用的交互式元素,但你可能需要更大的可复用的组件单元的来满足一个特殊的布局需要,为了有效的使用复用来完成布局,可以使用
<include/>
and
<merge/>
标签来使得另外一个布局单元嵌入到当前布局单元中
重用布局是一项特别有用的手段来让你有能力构建可重用复合型的布局,例如一个yes/no按钮,含有描述信息的自定义进度条。这个就意味着在你应用中的任何一个元素都可以给多个布局进行提取,分开管理,然后包含进各自的布局中。所以当你创建布局的私有UI组件来定制一个视图时,其实你可以复用一个布局单元来更容易的做到这个。
创建一个可以重用的布局单元
当你知道你需要重用的布局时,你可以创建一个新的xml布局文件来定义它,下面是一个需要被重用的布局单元,titlebar的布局文件
< FrameLayout xmlns:android ="http://schemas.android.com/apk/res/android"
android:layout_width ="match_parent"
android:layout_height= "wrap_content"
android:background= "#FF000000" >
<ImageView android:layout_width= "wrap_content"
android:layout_height ="wrap_content"
android:src ="@drawable/titlebar"
android:contentDescription ="@string/app_name" />
</ FrameLayout>
如果想让你的布局单元能在每一个布局中都能够复用,根标签应该恰当
使用<include/>标签
我们可以在布局中通过使用<include/>标签来添加复用组件
< LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android"
xmlns:tools= "http://schemas.android.com/tools"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:paddingBottom= "@dimen/activity_vertical_margin"
android:paddingLeft= "@dimen/activity_horizontal_margin"
android:paddingRight= "@dimen/activity_horizontal_margin"
android:paddingTop= "@dimen/activity_vertical_margin"
tools:context= ".MainActivity"
android:orientation= "vertical" >
<include layout= "@layout/titlebar"/>
<TextView
android:layout_width ="wrap_content"
android:layout_height ="wrap_content"
android:text ="@string/hello_world" />
</ LinearLayout>
你也可以在当前布局中复写重用组件的
android:layout_*
属性
<include
android:id ="@+id/news_title"
android:layout_width ="match_parent"
android:layout_height ="match_parent"
layout= "@layout/titlebar" />
使用<merge/>标签
<merge/>标签可以帮助你在你的视图层次中消除多余的视图组,当一个布局包含另外一个布局时。当你的主布局是一个垂直结构的LinearLayout包含两个连续的能被其它布局重用的视图,被你放置在布局中的两个可重用的视图都需要各自的根视图,使用另外一个LinearLayout来充当可重用视图的根视图时,会导致一个垂直结构的LinearLayout嵌套在另外一个垂直结构的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>
同样是使用<include/>标签来添加进布局中,这样Android系统并不会理会<merge/>标签,而是直接把两个Button放置在布局中,避免了不必要的嵌套。另外需要注意的是<merge/>只可以作为布局的根节点,当需要包含其它布局组件的布局本身是以<merge/>为根节点的话,需要将被导入的xml layout置于viewGroup中,同时需要设置attachToRoot为True
使用ViewStub
有些时候在我们的应用中可能会有一些极少使用到的复杂的视图,可能是一个列表项的详细信息,一个进度指示器,或者不用进行处理的信息,当我们在使用视图的时机去加载视图,而不是应用已启动就去加载,这样应用能够有效的减少内存的使用和加快视图的渲染速度
定义一个ViewStub
ViewStub 是一个轻量级的View,它是一个没有不占布局位置,看不见的,占用资源非常小的控件
<ViewStub
android:id ="@+id/stub_import"
android:layout_width ="fill_parent"
android:layout_height ="wrap_content"
android:layout_gravity ="bottom"
android:inflatedId ="@+id/item_details_import"
android:layout ="@layout/item_details" />
item_details.xml
<? 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" >
<TextView
android:id ="@+id/tv_stub_item_details"
android:layout_width= "wrap_content"
android:layout_height ="wrap_content"
android:text ="@string/item_details"
/>
</ LinearLayout>
使用后台线程AsyncTask更新UI,使用ViewHolder
使用AsyncTask
UI的渲染加载都是运行在Android程序的主线程中,如果UI主线程阻塞的时间超过5s,应用程序将会无响应,给用户带来极不好的体验,比如从服务器上获取一张图片等耗时操作都有可能导致我们的应用无响应,这时我们可以使用后台线程异步类AsyncTask来在后台加载数据更新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);
Android3.0后,AynscTask有了一个新特性,可以在多核CPU上同时执行多个后台任务, 但要调用
executeOnExecutor()方法来替代
execute()方法
使用ViewHolder
当我们滑动ListView的时候会频繁的调用findViewById()方法,这样会降低我们程序的性能。即使Adapter能够返回一个循环利用的已经渲染好的View,但依然需要去寻找View包含的元素并且需要更新它们,可以用"View Hodler"设计模式来解决反复使用findViewById()所带来的性能问题
ViewHodler能够储存convetView中每一个元素,当下次渲染的时候只需要从ViewHodler中直接取就行了,而不用反复的去
findViewById()
部分代码片段
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater
.inflate(R.layout.applist_item, null);
holder = new ViewHolder();
holder.icon = (ImageView) convertView
.findViewById(R.id.applist_item_iv_appicon);
holder.name = (TextView) convertView
.findViewById(R.id.applist_item_tv_appname);
holder.pk = (TextView) convertView
.findViewById(R.id.applist_item_tv_apppk);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.icon.setImageDrawable((Drawable) datalist.get(position).get(
"icon"));
holder.name.setText((String) datalist.get(position).get("name"));
holder.pk.setText((String) datalist.get(position).get("pk"));
return convertView;
}
static class ViewHolder {
ImageView icon;
TextView name;
TextView pk;
}