listview可以说是Android开发中最常见的UI控件了,listview能够以列表的方式显示大量同类的数据,这样问题就产生了,既然是大量数据,就会使用到很多布局,给布局绑定数据,listview将占用大量资源还可能会产生卡顿现象。
private String[] mArrData;
private TextView mTV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.main);
mTV = (TextView) findViewById(R.id.tvShow);
mArrData = new String[ 1000 ];
for ( int i = 0 ; i < 1000 ; i ++ ) {
mArrData[i] = " Google IO Adapter " + i ;
}
mAdapter = new TestAdapter( this , mArrData);
((ListView) findViewById(android.R.id.list)).setAdapter(mAdapter);
}
listview现在最常用也拥有很好的性能的优化方式是在Adapter中使用静态的ViewHolder,具体代码如下:
Activity
private
TestAdapter mAdapter;
private String[] mArrData;
private TextView mTV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.main);
mTV = (TextView) findViewById(R.id.tvShow);
mArrData = new String[ 1000 ];
for ( int i = 0 ; i < 1000 ; i ++ ) {
mArrData[i] = " Google IO Adapter " + i ;
}
mAdapter = new TestAdapter( this , mArrData);
((ListView) findViewById(android.R.id.list)).setAdapter(mAdapter);
}
Adapter
private
int
count
=
0
;
private
long
sum
=
0L
;
@Override
public View getView( int position, View convertView, ViewGroup parent) {
// 开始计时
long startTime = System.nanoTime();
ViewHolder holder;
if (convertView == null ) {
convertView = mInflater.inflate(R.layout.list_item_icon_text,
null );
holder = new ViewHolder();
holder.icon1 = (ImageView) convertView.findViewById(R.id.icon1);
holder.text1 = (TextView) convertView.findViewById(R.id.text1);
holder.icon2 = (ImageView) convertView.findViewById(R.id.icon2);
holder.text2 = (TextView) convertView.findViewById(R.id.text2);
convertView.setTag(holder);
}
else {
holder = (ViewHolder)convertView.getTag();
}
holder.icon1.setImageResource(R.drawable.icon);
holder.text1.setText(mData[position]);
holder.icon2 .setImageResource(R.drawable.icon);
holder.text2.setText(mData[position]);
// 停止计时
long endTime = System.nanoTime();
// 计算耗时
long val = (endTime - startTime) / 1000L ;
Log.e( " Test " , " Position: " + position + " : " + val);
if (count < 100 ) {
if (val < 1000L ) {
sum += val;
count ++ ;
}
} else
mTV.setText(String.valueOf(sum / 100L )); // 显示统计结果
return convertView;
}
}
static class ViewHolder {
TextView text1;
ImageView icon1;
TextView text2;
ImageView icon2;
}
@Override
public View getView( int position, View convertView, ViewGroup parent) {
// 开始计时
long startTime = System.nanoTime();
ViewHolder holder;
if (convertView == null ) {
convertView = mInflater.inflate(R.layout.list_item_icon_text,
null );
holder = new ViewHolder();
holder.icon1 = (ImageView) convertView.findViewById(R.id.icon1);
holder.text1 = (TextView) convertView.findViewById(R.id.text1);
holder.icon2 = (ImageView) convertView.findViewById(R.id.icon2);
holder.text2 = (TextView) convertView.findViewById(R.id.text2);
convertView.setTag(holder);
}
else {
holder = (ViewHolder)convertView.getTag();
}
holder.icon1.setImageResource(R.drawable.icon);
holder.text1.setText(mData[position]);
holder.icon2 .setImageResource(R.drawable.icon);
holder.text2.setText(mData[position]);
// 停止计时
long endTime = System.nanoTime();
// 计算耗时
long val = (endTime - startTime) / 1000L ;
Log.e( " Test " , " Position: " + position + " : " + val);
if (count < 100 ) {
if (val < 1000L ) {
sum += val;
count ++ ;
}
} else
mTV.setText(String.valueOf(sum / 100L )); // 显示统计结果
return convertView;
}
}
static class ViewHolder {
TextView text1;
ImageView icon1;
TextView text2;
ImageView icon2;
}
在Adapter的代码中,在getView方法里首先判断convertView是否为空,若为空则加载相应布局,若不为空则
直接使用该布局,这能够很有效的使用Android为listview提供的缓存机制:只加载一屏的布局,之后滑动出来的item使用的是之前已经加载的布局的缓存;
而使用静态的ViewHoulder的目的则是节省了findViewById的时间。如果不使用ViewHolder,每次getView的时候都需要得到一次子布局,而这也是很耗时并且耗资源的;如果使用了ViewHolder作为子布局的缓存,使用View的setTag方法将缓存与每个item绑定,则也可以省去了findViewById的事件;而将ViewHolder设置为static的目的是指在初始化Adapter时初始化一次这个内部类,否则将会在每次创建Adapter时都要初始化一次,而这是没有必要的。
上述方法能够解决大部分listview消耗资源以及卡顿的问题,但对于不同的需求的listview来说还会存在其他让listview卡顿的原因,比如listview的item每次加载时都需要获得图片并设置到imageview中,item加载时需要进行大量的计算,item里的TextView需要设置指定字体;这些耗时的操作都会让listview滑动起来很卡,带来不好的体验;
这几天我一直在研究Android4.0+系统联系人的源码,因为我发现,系统联系人采用的也是listview布局,每一个item都有图片文字,而且使用了fastscroller模式,对联系人以首字母进行了分来,同时Adapter还实现了SectionIndexer接口,能够实现这种效果;
但是却一点都不卡,而我自己做的一个类似的界面,却十分卡顿,很影响用户体验;于是我找来了Android体统联系人的源码,自己建立起来了一个可以运行的程序,然后去研究它是如何实现这么流畅的listview的;
经过研究我发现系统联系人的listview使用的是自定义的listview:PinnedHeaderListView 它的定义是这样的:
/**
* A ListView that maintains a header pinned at the top of the list. The
* pinned header can be pushed up and dissolved as needed.
*/
它给listview加了一个header即显示在联系人界面最上方贴着屏幕顶部的那部分,同时它继承自AutoScrollListView 而这个AutoScrollListView继承自listview,对listview进行了一些优化,让listview在互动到指定的item时更流畅;
看到这里,于是我使用了这个AutoScrollListView ,但效果不是很明显,listview还是很卡;在系统联系人使用的listview中没有找到解决方法,我开始研究它的Adapter;研究Adapter发现,系统的Adapter进行了很多层的封装,完全淡化了geiView方法;使用了很多AsyncTask来加载不同的数据,然后使用了CursorLoader来将加载好的数据添加到Adapter里,同时还将图片的加载与数据的加载进行了分离,代码逻辑十分复杂;但通过这个我得出了一个结论:不要将任何的耗时操作放在listview的getView方法里。
在系统联系人的这些Adapter里我发现getView方法中没有任何的耗时操作,在设置图片时图片已经得到,对列表按照字母进行的分类也已经分类好了,存放在一个内部类里;在得出这个结论之前我尝试过很多系统联系人中的代码,但都没有得到明显的效果,经过大量的测试我得出了这个结论,并且在测试中得到了验证。
在我的项目中,listview的每一个item都有一个图片,和很多TextView,而且所有的TextView都要设置非系统的字体;Adapter使用的是ViewHolder优化,在getView中的代码已经很少了,但是还是卡;我的listview中的数据是一个对象的List,在对象里只存放了item需要展示的图片的资源ID,或者是图片的路径,需要通过一些操作才能获得图片,而这些操作其实是很耗时的;于是我将原来的对象进行了更改,将图片对象直接存放在item对应的对象中,然后再Adapter初始化的时候将这些对象初始化,虽然listview展示所需的时间稍微长了一点,但是结果是listview滑动流畅了很多;接着我又将从assets中获得字体TypeFace的操作放在了Adapter初始化的方法中,并且将字体通过静态的变量都存起来,然后再getView中只需为TextView设置一下taptface即可,不需要在从asset中获取字体所花费的时间;通过上面两步操作之后,我的listview一点都不卡了,十分流畅(将图片放在List的每个数据项里虽然能解决问题,但不是一个很好的解决方案,后来我使用了内存缓存加上异步加载图片的方式也保证了列表的流畅)。
我这里提供我使用的内存缓存的一个实现:
public class MemoryCache {
private static final String TAG = "MemoryCache";
private Map<String, Bitmap> cache=Collections.synchronizedMap(
new LinkedHashMap<String, Bitmap>(10,1.5f,true));//Last argument true for LRU ordering
private long size=0;//current allocated size
private long limit=1000000;//max memory in bytes
public MemoryCache(){
//use 25% of available heap size
setLimit(Runtime.getRuntime().maxMemory()/4);
}
public void setLimit(long new_limit){
limit=new_limit;
Log.i(TAG, "MemoryCache will use up to "+limit/1024./1024.+"MB");
}
public Bitmap get(String id){
try{
if(!cache.containsKey(id))
return null;
//NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78
return cache.get(id);
}catch(NullPointerException ex){
ex.printStackTrace();
return null;
}
}
public void put(String id, Bitmap bitmap){
try{
if(cache.containsKey(id))
size-=getSizeInBytes(cache.get(id));
cache.put(id, bitmap);
size+=getSizeInBytes(bitmap);
checkSize();
}catch(Throwable th){
th.printStackTrace();
}
}
private void checkSize() {
Log.i(TAG, "cache size="+size+" length="+cache.size());
if(size>limit){
Iterator<Entry<String, Bitmap>> iter=cache.entrySet().iterator();//least recently accessed item will be the first one iterated
while(iter.hasNext()){
Entry<String, Bitmap> entry=iter.next();
size-=getSizeInBytes(entry.getValue());
iter.remove();
if(size<=limit)
break;
}
Log.i(TAG, "Clean cache. New size "+cache.size());
}
}
public void clear() {
try{
//NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78
cache.clear();
size=0;
}catch(NullPointerException ex){
ex.printStackTrace();
}
}
long getSizeInBytes(Bitmap bitmap) {
if(bitmap==null)
return 0;
return bitmap.getRowBytes() * bitmap.getHeight();
}
}
综上,listview的优化其实就是去找getView中的耗时操作,然后提取出来,要么使用异步的方式为item的布局设置数据,要是实在需要同步,就只能在Adapter初始化时将数据准备好,然后再getView中只需绑定一下就行。