前言
今天帶大家過一遍ListView常用的優化方案,重點在於解決ListView的item中包含異步加載圖片時遇到的圖片閃爍和顯示錯亂等情況.
ListView的item回收和重用
Android系統爲了使得ListView性能優化,會爲ListView增加item行緩存.簡單來說,假設ListView有1萬個item,但是Android只爲其創建n個item(n爲當前屏幕能顯示的item的數量).ListView通過adapter的getView函數獲取每行的item.滑動過程中:
- 如果某行item已經劃出屏幕,若該item在緩存內,則更新緩存內容.否則,將item通過put方法放入緩存中.
- 獲取劃入屏幕的行item之前,會首先判斷緩存中是否有可以的item.如果有,則作爲convertView參數傳遞給adapter的getView函數.
瞭解了上面的大概,讓我們從源碼的角度來分析一下ListView的緩存機制.具體需要參考AbsListView的obtainView方法.
在這個方法中,有個特別重要的Field:
final RecycleBin mRecycler = new RecycleBin();
在obtainView方法中,我們就是通過mRecycler來獲取緩存View的.
scrapView = mRecycler.getTransientStateView(position);
if (scrapView == null) {
scrapView = mRecycler.getScrapView(position);
}
RecycleBin是一個內部類,這個類是幫我們複用item的關鍵.RecycleBin將每行item分爲兩類: ActiveViews和ScrapViews.其中ActiveViews是一開始顯示在屏幕上的View,ScrapViews是潛在的可以讓adapter使用的old views,也就是緩存view.
ActiveViews和ScrapViews表明AbsListView緩存依賴於這兩個數組,ActiveViews用於存儲屏幕上當前顯示的Item,ScrapViews用於存儲從屏幕移除可能會被複用的Item.
我們先來看一下, ActiveViews是如何產生的.
ActiveViews
在RecycleBin類中,是通過fillActiveViews來創造ActiveViews的.源碼如下:
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i ++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
}
}
}
在ListView.java中的layoutChildren方法中,我們可以看到fillActiveViews的調用:
public void layoutChildren() {
int childCount = getChildCount();
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i ++) {
recycleBin.addScrapView(getChildAt(i));
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
}
可以看出,如果數據發生變化則把當前的item放入到ScrapViews中,否則把當前顯示的item放入ActiveViews中.
ScrapViews
在上面的代碼中,我們主要到,當dataChanged時候,ListView開始構建ScrapView.
ViewHolder
同時,爲了進一步提高效率,我們可以使用自定義的ViewHolder對象來緩存item的view對象,節省每次去findViewById的時間.參考代碼如下:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
viewHolder = new ViewHolder();
viewHolder.mImg = (ImageView) convertView.findViewById(R.id.id_img);
viewHolder.mTitle = (TextView) convertView.findViewById(R.id.id_title);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder)conview.getTag();
}
viewHolder.mImg.setImageBitmap(mBitmap);
viewHolder.mTitle.setText("111");
}
private static class ViewHolder {
ImageView mImg;
TextView mTitle;
}
ListView的item緩存和ViewHolder的控件緩存雖然提高了ListView的滑動性能,但是當item存在異步圖片加載的情況時,同樣也帶來了很多問題.
ListView的圖片異步加載問題
ListView中當item存在圖片異步加載時,可能出現的問題包括:
- 行item的圖片顯示錯亂
- 行item的圖片顯示閃爍
問題原因
雖然圖片異步加載可能出現的問題很多,但是導致問題出現的原因是一樣的.
我們假設當前手機一屏能顯示10個item.此時,item1正在異步加載圖片,這時用戶滑動的手機屏幕,當item1消失,item11進入時,item11有可能複用item1的view.由於圖片異步加載通常涉及到網絡請求會比較慢,而item11又恰好複用了item1,此時item1的圖片纔剛剛進行了顯示.然後item11又通過異步加載加載屬於item11的圖片,item11的圖片再次顯示遮蓋住item1的圖片,因此會出現圖片的顯示錯亂和閃爍.
解決思路
通過上面的分析,我們知道圖片錯亂或者閃爍的原因都是由於圖片異步加載太慢和異步加載過程中ListView的item複用導致的.因此我們的解決方案是:
- 提高圖片的異步加載速度,增加內存和硬盤二級緩存,這裏我推薦直接使用Volley的ImageLoader即可.
- 我們可以在getView函數中給每個ImageView增加一個tag來標識圖片來源,這樣異步加載完成後我們先比較圖片來源和需要加載的圖片是否一致,如果一致再顯示,不一致則不做處理.
提高圖片異步加載速度
首先,Volley已經爲我們提供了圖片的本地存儲,我們只需要爲其增加內存緩存即可,這裏推薦使用LruCache作爲內存緩存.
示例代碼(BitmapCache.java):
public class BitmapCache implements ImageLoader.ImageCache {
private LruCache<String, Bitmap> mCache;
/**
* 初始化BitampCache緩存類.
* 默認使用當前進程內存空間的1/8
*/
public BitmapCache() {
this((int) (Runtime.getRuntime().maxMemory() / 8));
}
/**
* 初始化BitampCache緩存類.
*
* @param maxSize 緩存類默認存儲大小,單位爲字節
*/
public BitmapCache(int maxSize) {
mCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String s) {
return mCache.get(s);
}
@Override
public void putBitmap(String s, Bitmap bitmap) {
mCache.put(s, bitmap);
}
}
使用BitmapCache構造Volley的ImageLoader:
ImageLoader this.mImageLoader = new ImageLoader( VolleyManager.getInstance(context).getRequestQueue(), new BitmapCache());
增加圖片來源標識
我們使用Volley中提供的默認ImageListener是無法增加圖片來源標識的.
首先,給出Volley默認的ImageListener構造源碼:
public static ImageLoader.ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId) {
return new ImageLoader.ImageListener() {
public void onErrorResponse(VolleyError error) {
if(errorImageResId != 0) {
view.setImageResource(errorImageResId);
}
}
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
if(response.getBitmap() != null) {
view.setImageBitmap(response.getBitmap());
} else if(defaultImageResId != 0) {
view.setImageResource(defaultImageResId);
}
}
};
}
從源碼可以看出,當我們通過ImageLoader.getImageListener方法獲取ImageListener,ImageListener再爲ImageView設置圖片顯示時並沒有判斷圖片來源.
既然已經看到了源碼,那我們只需要參考其實現,併爲其增加判斷圖片來源的函數即可,這裏我使用網絡圖片的url作爲唯一標識:
private ImageLoader.ImageListener createOptimizeImageListener(
final String imageUrl, final ImageView view, final int defaultImageResId,
final int errorImageResId) {
return new ImageLoader.ImageListener() {
public void onErrorResponse(VolleyError error) {
String imageUrlTag = (String) view.getTag();
if (ObjectUtils.isEquals(imageUrl, imageUrlTag)) {
if (errorImageResId != 0) {
view.setImageResource(errorImageResId);
}
}
}
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
String imageUrlTag = (String) view.getTag();
if (ObjectUtils.isEquals(imageUrl, imageUrlTag)) {
if (response.getBitmap() != null) {
view.setImageBitmap(response.getBitmap());
} else if (defaultImageResId != 0) {
view.setImageResource(defaultImageResId);
}
}
}
};
}