前言
相信Android開發者對ListView不會陌生,使用ListView需要設置相應的Adapter才能展示數據。Adapter到底是什么東西?讓我們來一探究竟。
Adapter

通過圖1我們可以看出 Adapter是View和數據之間的橋梁,並為每一個數據項生成相應的View。Adapter是個接口,定義了子類需要實現的方法,最常見的
方法有:
getCount(),總共有多少數據項
getItem(),獲取對應position 中的item
getView(),返回需要展示在屏幕中的View
一般在自定義Adapter中,只需要實現上述三個方法即可。
Adapter優化前
class ImageAdapter extends BaseAdapter {
private Context mContext ;
private String[] mList ;
public ImageAdapter(Context context, String[] list) {
mList = list ;
mContext = context ;
}
@Override
public int getCount() {
return mList.length;
}
@Override
public Object getItem(int position) {
return mList[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(mContext).inflate(R.layout.gridview_item, parent, false) ;
TextView tv = view.findViewById(R.id.text) ;
tv.setText(mList[position]);
return view;
}
}
上述大概就是一個最簡單的Adapter的實現了吧。通過在getView函數中inflate一個新布局,給相應position設置data,然后將view返回給父控件。咋一看沒啥問題,運行也不會有錯。但是會有嚴重的性能問題。想象一下,如果有mList中有100萬條數據,我們有必要每次都重新inflate一個新layout,生成一個新view嗎?顯然是沒有必要的,我們的手機屏幕就那么小,可見的View其實也就那幾個。那些看不見的View其實是沒必要保存的。
View復用原理

圖片來源於此 博客。上圖清晰展現了View的復用原理。手機屏幕一共能夠展示七個item,繼續往上滑動,item 1從我們視線消失,此時ListView 會調用Adaper中的getView函數來生成第八個item。此時getView函數中參數convertView就是item 1,我們只需把convertView(item 1)上的數據項全部設置成item 8的數據項即可,這樣就不用再重新inflate一個新View出來了。不管是有mList有多大,內存中保存的View的個數永遠只是可見的幾個。這對程序性能有很大提升。
Adapter優化后
class ImageAdapter extends BaseAdapter {
private Context mContext ;
private String[] mList ;
public ImageAdapter(Context context, String[] list) {
mList = list ;
mContext = context ;
}
@Override
public int getCount() {
return mList.length;
}
@Override
public Object getItem(int position) {
return mList[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null ;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.gridview_item, parent, false) ;
viewHolder.textview = (TextView) convertView.findViewById(R.id.text);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
// 綁定對應position數據
bindViewData(position, viewHolder) ;
return convertView;
}
private void bindViewData(int position, ViewHolder viewHolder) {
viewHolder.textview.setText(mList[position]);
}
private static class ViewHolder {
public TextView textview ;
}
}
以圖2為例,每個item都是由View(視圖)和Data(數據)組成的。前7個item的convertView都是為空的,因此inflate出了7個新View,當第8個item變成可見時此時convertView 為item 1,再通過bindViewData函數把第八個mList中的數據設置給item 1對應的View,然后直接返回convertView。此時我們看到的item 8其實是由這兩個東西組成的:
View ------ item 1 的View
Data ------ mList 中的第八項
因為item 8和View 和item 1的View結構是一樣的(使用同樣layout),所以不會造成錯誤。
View復用導致的問題
問題1:item的狀態錯亂

如圖3所示,6跟帖這個View不是所有item都有的,還是拿圖2來舉例。假設item 1是圖3的第一項,item8是圖3的第二項。item 1中跟帖這個View是顯示的,因為item 8使用的是item 1的View,所以item 8中跟帖的View的狀態也是顯示的。但是在item 8中跟帖這個View是不應該顯示的。這就是復用View導致item狀態錯亂的問題。解決方法之一就是在bindViewData函數中給layout中的View先全部還原成默認狀態即可。
問題2:多線程導致圖片加載位置錯亂
圖片加載往往涉及到網絡操作,因此ListView中加載圖片時一般都會開啟新線程去加載圖片。如果不使用View復用方法,直接使用優化前的Adapter,是不會有任何問題的。但如果使用的View復用,就不一定了。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null ;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.gridview_item, parent, false) ;
viewHolder.imageView = (ImageView) convertView.findViewById(R.id.square_image_view);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
String url = mList[position] ;
ImageView imageView = viewHolder.imageView ;
ImageLoader.loadBitmap(imageView, url);
return convertView;
}
還是拿圖2為例。如上述代碼所示,我們使用了復用View方法,並且異步加載圖片。由於網絡比較慢,item 1到item 7的圖片還沒加載出來,我們滑動到了item 8(復用了item 1的ImageView),突然網絡變好了,item 8 的圖片加載完成,我們給Item 8設置了圖片。過了一會,item 1的圖片也下載完成了,我們又給item 1設置了圖片,由於item 1和item 8共用同一個ImageView,因此item 8的圖片立馬變成了item 1的圖片。此時item 8上顯示的竟然是item 1對應的圖片!解決辦法之一就是給ImageView setTag。代碼如下:
ImageView imageView = viewHolder.imageView ;
String tag = (String) imageView.getTag();
String url = mList[position];
if ( !url.equals(tag) ) {
//default drawable
imageView.setImageDrawable(null);
}
// set tag to image view
imageView.setTag(url);
// load bitmap
ImageLoader.loadBitmap(imageView, url);
延伸閱讀
關於 ScrapView 和 ActiveView
Performance Tips for Android’s ListView
Handling ListView Recycle on Android