ListView优化
通过上一篇文章《ListView原理分析》我们对ListView有了进一步的了解,知道了ListView如何进行View复用,以达到展示数万条数据而不发生OOM的目的。但是,在我们在使用ListView时仍然有一些需要注意的地方,并且,通过优化,我们可以使ListView具有更好的性能。
首先引用一段Google的官方建议:
使ListView流畅滑动的关键是让主线程远离耗时任务,你要确保你的磁盘访问、网络访问以及SQL查询等操作在子线程中进行。你可以通过启用StrictMode来测试你的程序。
此外,Google还给了两个例子,一个是使用子线程,一个是使用ViewHolder。
// 使用子线程
// 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);
//使用ViewHolder
static class ViewHolder {
TextView text;
TextView timestamp;
ImageView icon;
ProgressBar progress;
int position;
}
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);
ConvertView
严格来说,使用ConvertView并不能算是对ListView的优化,因为你必须要使用ConvertView来完成View的复用。《ListView原理分析》这篇文章里已经对ListView的原理进行了分析,因此此处不在赘述使用ConvertView的必要性,这里给出一个使用ConvertView的示例:
public View getView(int position, View convertView, ViewGroup parent){
View v;
if (convertView == null) {
v = inflater.inflate(resource, parent, false);
} else {
v = convertView;
}
bindView(position, v);
return v;
}
ViewHolder
使用ViewHolder是Google官方的建议,它也几乎是所有ListView优化中必做的操作。通过使用ViewHolder,我们可以避免每次复用View时通过调用findViewById来查找每个子View,而是可以直接获取到需要的子View,然后为其绑定数据。文章开篇给出了Google提供的示例,这个给出一个完整的ViewHolder使用示例。
static class ViewHolder {
TextView text;
……//
int position;
}
public View getView(int position, View convertView, ViewGroup parent){
View v;
ViewHolder holder;
if (convertView == null) {
v = inflater.inflate(resource, parent, false);
holder = new ViewHolder();
holder.text = (TextView) v.findViewById(R.id.listitem_text);
……//
holder.position = position;
v.setTag(holder);
} else {
v = convertView;
holder = (ViewHolder)v.getTag();
}
bindView(position, holder);
return v;
}
public void bindView(int position, ViewHolder holder){
if(holder.position == position)
return;
holder.position = position;
holder.text.setText(……);
……//
}
数据加载
如果我们的ListView只有少量的数据来显示,那么直接加载所有数据或许是可行的。但是,如果我们用ListView展示成千上万的数据,并且这些数据来自磁盘、网络等慢速存储设备,那么我们获取数据的操作就会是一个耗时操作,并且有限且宝贵的内存资源使得加载全部数据到内存变的不可行。
考虑如下两种情况:
- 由于在ListView滑动的过程中会频繁的调用getView,因此,如果在每次调用getView时去执行耗时操作加载数据,那么滑动过程势必会由于不断执行耗时任务而引起卡顿;
- 再考虑这样一种情况,用户在一个带宽很小的网络环境下,我们按顺序加载数据,那么,当用户滑动到第1000条数据时,我们可能才加载到第100条数据,因此用户不得不面对空白的ListView等待,而且我们加载的其它数据用户可能根本没有机会看到,这无疑浪费了用户的流量和宝贵的 内存。
因此,为了使ListView能够流畅地响应滑动,我们需要做以下优化:
- 通过异步的方式来处理耗时的数据加载任务,就像在文章开篇介绍的那样,将耗时的数据加载过程放到异步任务里;
- 此外,在有大量数据需要加载的情况下,我们只有异步任务是不够的,我们还需要按需执行异步加载。我们可以给ListView设置OnScrollListener,然后在onScrollStateChanged方法中判断滑动状态,如果列表正在滑动,那么就停止数据加载,直到列表静止,开始加载列表目前所在position的数据。
其它
局部刷新:通常,我们数据集改变之后会通过Adapter的notifyDataSetChanged方法来通知ListView,而ListView的会去调用requestLayout请求重新布局,显然会做很多无用功。很多时候,我们只是想更新某一条数据,那么显然调用notifyDataSetChanged会造成比较大的开销。因此,我们可以继承BaseAdapter来实现局部刷新功能,主要思路是,当某一个position处的数据改变时,我们直接操作数据集,更改此处的数据,然后,判断该position是否正显示在屏幕上(通过ListView的getFirstPosition和getChildCount),如果正在显示,则通过setXXX来重新设置该View的显示的数据。
监听器:如果我们的ListView中显示的组件有使用OnXxxListener的需求,那么我们也可以将Listener绑定到ViewHolder来减少Listener的创建。