最近在折腾ListView的优化,发现网上有许多优秀的实现方法,但是分散在不同的代码中,希望通过这一系列文章做个总结,并梳理清楚脉络。
1、ListView是怎么工作的
ListView的设计考虑了可拓展性和性能,从本质上来说,这意味着:
(1)尽量少的inflate操作
(2)只绘制或展示屏幕上可见(或者即将可见)的子控件
ListView为了实现第(2)条,当列表滚动的时候,ListView通过视图回收器从回收池中取出视图添加到屏幕中,并且把滑出屏幕范围的视图缓存回回收器中
(源码实现)
这样,即使你有成百上千行的数据,ListView只需在内存中保持一定(少数)的行视图对象(包括显示中的和回收器里面的),它会以不同的方式把
每一行填充到列表中,从上方往下或者从下方往上,这取决于列表的如何滚动的。上图便是列表往下滚动时的可视化流程。
熟悉以上框架之后,便可以上手一些优化技巧了,正如你所看到的那样,在列表滚动时ListView动态地inflate和回收很多视图对象,所以保证适配器
getView()方法更加轻量级是关键,下面所有的方法都围绕着如何使得getView()方法更快速来展开。(前两种方法在上一篇文章中简单提到过)
2、视图回收利用
每次ListView需要在屏幕上增加新的一行,它便会通过适配器调用getView()方法,正如你所知道的那样,getView()方法有三个参数:行位置position;
行视图对象convertView;父控件ViewGroup。
参数convertView就是之前提到的“ScrapView”,当回收器中有缓存时会返回一个非空值,因此,如果convertView非空的时候,你只需要更新它的内容,
而不是inflate一个新的行布局视图,加入回收利用机制的getView()的代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.your_layout, null);
}
TextView text = (TextView) convertView.findViewById(R.id.text);
text.setText("Position " + position);
return convertView;
}
3、ViewHolder模式
从一个被inflate出来的布局中寻找内部视图是Android开发中的常见操作之一,通常通过findViewById()视图方法实现,该方法会递归遍历视图树寻找
给定ID对应的子视图。在静态UI布局上使用findViewById()很好,但是随着列表的滚动,ListView频繁地调用适配器的getView()方法.而findViewById()
对滚动的性能有着较大的影响,特别是当你的行布局比较复杂的时候。
ViewHolder模式的核心是减少适配器getView()方法中调用findViewById()方法的次数,实际上,HoleView是保存着行视图的内部子视图的直接引用的轻量级内部
类,在对行视图进行inflate操作之后,可以把它作为一个标记(TAG)保存在行视图(convertView)中,这样你只需要在创建视图布局的时候调用
findViewById()方法。下面是加入了ViewHolder模式之后的代码示例:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.your_layout, null);
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.text);
convertView.setTag(holder);
} else {
holder = convertView.getTag();
}
holder.text.setText("Position " + position);
return convertView;
}
private static class ViewHolder {
public TextView text;
}
getView()方法已经做了优化,所以很多聪明的想法其实是不必要的,例如:
(1)本地缓存视图,当某个位置总是显示同样元素的时候,便把该位置对应的视图缓存起来供下次同样位置的请求使用。
(2)猜测getView()调用顺序,因为getView()的调用顺序是没有保证的(为了提高性能),有几种方法展示ListView,有时候从底部开始,有时候从顶部
开始,有时候从中间开始,也就是说你得到的最后一个视图未必是屏幕最下方的视图。
4、异步加载
它们做了缓存。但是有时候你可能希望展示动态内容(来自本地磁盘或者网络),例如短文或者资料图片等。这种情况下,你应该不希望直接在getView()
方法
中加载它们,因为任何情况下都不应该让IO操作阻塞UI线程,这样做会影响列表滚动的流畅性。
你希望的是在独立的线程中异步处理每一行的IO或者给CPU带来很大负担的操作,这里的技巧在于这样处理的同时遵循ListView的回收利用行为。例如,如果
你在适配器getView()方法中执行异步任务加载图片,在异步任务完成之前,该图片对应的视图可能会被回收并且被另一个位置利用,于是乎你需要一种
机制检验在异步任务完成的时候视图是否已经被回收。
一种简单的检验方法是再视图上附加信息标记哪一行与它关联,然后你可以检验当异步任务结束时与视图关联的目标行是否仍然相同。有很多种方法可以
实现这种机制,下面提供一种简化的实现:
public View getView(int position, View convertView,
ViewGroup parent) {
ViewHolder holder;
...
holder.position = position;
new ThumbnailTask(position, holder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
return convertView;
}
private static class ThumbnailTask extends AsyncTask {
private int mPosition;
private ViewHolder mHolder;
public ThumbnailTask(int position, ViewHolder holder) {
mPosition = position;
mHolder = holder;
}
@Override
protected Cursor doInBackground(Void... arg0) {
// Download bitmap here
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (mHolder.position == mPosition) {
mHolder.thumbnail.setImageBitmap(bitmap);
}
}
}
private static class ViewHolder {
public ImageView thumbnail;
public int position;
}
5、交互意识(Interaction awareness)
对每一行异步加载重量级资源是提高ListView性能的重要步骤,但是如果你在滚动时盲目地为每一个getView()操作开启异步任务,将会浪费
大量的资源,因为由于行视图经常被回收导致很多结果都被丢弃。我们需要为ListView适配器添加交互意识,使得它再某些操作之后不会对每
一行都执行异步任务,例如在ListView上的甩动动作(也就是快速滑动,这时候对每一行执行异步任务是没有意义的),当滑动停止,或者将要
停止时,就是为每一行实际展示重量级内容的时候。
由于相关代码比较长,我就不展示出来了,Romain Guy 经典的Shelves app 是一个很好的例子,其中展示了当GridView停止滚动的时候,就会
触发书本封面的加载。还可以平衡内存
缓存与交互意识之间的使用,在滚动的时候展示缓存内容。
6、结束语
强烈推荐看看谷歌工程师Romain Guy和Adam Powell关于ListView的演讲,其中也包含了上面提到的许多东西。如果你想看看这些技巧的实际运用,
可以看看Pattrn。虽然本博文对于这些技巧没有新的发现,但我认为把它们编写到一块是有用的,希望它能成为黑客们开始进行安卓开发的有用
指引。
参考http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/