在項目優化過程當中,經過MAT監控發現存在一處內存泄露,反覆進入某個頁面,內存佔用愈來愈大。後分析找到了泄露緣由,原來是在自定義列表中,將行佈局的layout文件inflate成view對象的時候,每加載一次列表就要new出一組新的view對象。由於沒有對這些佈局一致的view進行復用,又無法及時釋放,致使了列表的行佈局對象越積越多,形成內存泄露。
java
解決這個oom問題,首先想到了listview加載中對convertview的回收和複用的方法。因而模仿convertview的原理寫了個對view對象進行回收和複用的類,此處類名爲ViewRecycler,使用後有效的解決了view對象的複用,遠離這個棘手的oom問題。因爲項目中須要複用的view對象佈局都是同樣的,此方法只考慮了複用同一佈局的狀況。同時,項目中的列表已有獲取顯示行與隱藏行的相應接口,此方法僅主要從回收與複用的邏輯層面加以實現,並未涉及任何底層代碼部分。數組
1、如下是具體的實現過程:ide
1.首先ViewRecycler類須要兩個容器分別用來保存活動的View對象和回收可複用的View對象。一個map用來保存回收的View(無序添加),一個數組用來記錄當前顯示的行號以及對應的View(有序添加)。佈局
以下:性能
private SparseArray<View> recycleViews;// 廢棄的view private View[] activeViews = new View[0];//正在使用的view
注:key爲int類型的HashMap用SparseArray代替,會有更好的性能.優化
2.每次列表刷新或變化,就更新一次activeViews的大小。即activeViews的數組長度與當前列表的總行數一致。在刷新列表或者加載更多時,調用getCount()方法更新activeViews數組長度。this
以下:spa
// 加載完畢 private void loadComplete() { //..... mViewRecycler.getCount(mDataList.size()); }
ViewRecycler類裏的getCount方法:調試
/** 獲取活動view的總數 */ public void getCount(int count) { final int length = this.activeViews.length; if (count > length) { final View[] activeViews = this.activeViews; this.activeViews = Arrays.copyOf(activeViews, count); // Log.e("getCount", "activeViews[" + (count - 1) + "]=" + this.activeViews[count-1]); } }
3.列表每新增顯示一行,就先獲取是否有可複用的View對象。先判斷recycleViews是否已存有該行號對應的View,沒有則獲取最新回收的View。再結合setTag與getTag即可實現對回收View對象的複用了。若是recycleViews沒有可複用的View,則inflate生成新的View。code
以下:
public void convertFromView(final ListJson listJson, final int n) { ViewHolder holder = null; // 判斷是否有可重複利用的view View resycleView = mViewRecycler.getRecycleView(n); if (resycleView == null) { resycleView = LayoutInflater.from(getActivity()).inflate(R.layout.list_item, null); holder = new ViewHolder(); //..... holder.iv_main = (ImageView) resycleView.findViewById(R.id.item_iv_main); resycleView.setTag(holder); } else { holder = (ViewHolder) resycleView.getTag(); } //保存新增活動的View對象 mViewRecycler.addActiveView(n, resycleView); // 加載行佈局數據 holder.tv_title.setText(lison.getName()); holder.tv_price.setText("待定"); //..... // 下載圖片 ImageLoadingListener listener = new ImageLoadingListener() { @Override public void onLoadingStarted(String arg0, View arg1) { } @Override public void onLoadingComplete(String arg0, View arg1, Bitmap bitmap) { //獲取view對象 View bitmapView = mViewRecycler.getActiveView(n); if (bitmapView != null) { ViewHolder holder = (ViewHolder) bitmapView.getTag(); if (holder != null) { // 加載圖片 holder.iv_main.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bg_item)); } } } @Override public void onLoadingFailed(String arg0, View arg1, FailReason arg2) { } @Override public void onLoadingCancelled(String arg0, View arg1) { } }; p_w_picpathLoader.loadImage(listJson.getStatusPic(), listener); }
ViewRecycler類裏的對應方法:
(1)將行號與對應的View填充到activeViews數組裏保存。添加前對行號與activeViews的長度進行校訂,避免越界。
/** 添加記錄當前活動的view */ public void addActiveView(int position, View view) { final int length = this.activeViews.length; if (position > length - 1) { getCount(position + 1); } this.activeViews[position] = view; // Log.e("addActiveView", "activeViews.size() = " + Arrays.toString(activeViews)); }
(2)根據行號獲取對應的View對象。
/** 獲取某個活動view */ public View getActiveView(int position) { final int length = this.activeViews.length; if (position > length - 1) { getCount(position + 1); } return activeViews[position]; }
(3)獲取已回收的View對象。
/** 獲取回收的view */ View getRecycleView(int position) { return retrieveFromRecycle(recycleViews, position); } /** 檢索回收的view */ static View retrieveFromRecycle(SparseArray<View> recycleViews, int position) { int size = recycleViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i = 0; i < size; i++) { int fromPosition = recycleViews.keyAt(i); View view = recycleViews.get(fromPosition); if (fromPosition == position) { recycleViews.remove(fromPosition); return view; } } int index = size - 1; View r = recycleViews.valueAt(index); recycleViews.remove(recycleViews.keyAt(index)); return r; } else { return null; } }
4.列表每隱藏一行,將消失的行佈局對應的View對象回收,添加到recycleViews容器裏,同時移除activeViews裏的這個View。而後再進行比較並清除recycleViews容器裏的回收對象,保證回收對象總數很少於活動view容器的總長度。
以下:
// 隱藏 @Override public void onInvalidateItem(int id) { super.onInvalidateItem(id); // ..... // 回收view對象 View recycleView = mViewRecycler.getActiveView(id); if (recycleView != null) { mViewRecycler.addRecycleView(id, recycleView); } }
ViewRecycler類裏的對應方法:
/** 添加廢棄的view,無序添加 */ void addRecycleView(int position, View scrap) { recycleViews.put(position, scrap); final int length = this.activeViews.length; if (position < length) { this.activeViews[position] = null; } pruneRecycleViews(); // Log.e("Recycle", "Recycle.size() = " +recycleViews.size()); }
/** 確保廢棄的view總數很少於活動的view容器的長度(此方法可再改進爲很少於當前活動的View對象數量) */ private void pruneRecycleViews() { final int maxViews = activeViews.length; int size = recycleViews.size(); final int extras = size - maxViews; size--; for (int j = 0; j < extras; j++) { recycleViews.remove(recycleViews.keyAt(size--)); } }
5.退出時,清除全部View對象及其引用。
如下方法則是根據項目需求,將全部的活動view移到recycleViews裏,再整理recycleViews。
以下:
@Override public void onDestroy() { super.onDestroy(); //銷燬全部view對象 mViewRecycler.recycleAllActiveViews(); }
ViewRecycler類裏的對應方法:
/** 將全部剩餘的活動view移到廢棄view裏 */ void recycleAllActiveViews() { final View[] activeViews = this.activeViews; SparseArray<View> recycleViews = this.recycleViews; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { activeViews[i] = null; recycleViews.put(i, victim); } } pruneRecycleViews(); }
2、調試結果
滑動時,請求加載行數據。每次添加一個行佈局對象,當達到10行後開始執行View對象回收。每次將棧底的View回收並複用到棧頂的行佈局裏。以下圖:
3、最後附上完整的ViewRecycler類
以下:
public class ViewRecycler { private SparseArray<View> recycleViews;// 廢棄的view private View[] activeViews = new View[0];//正在使用的view public ViewRecycler() { recycleViews = new SparseArray<View>(); } /** 獲取活動view的總數 */ public void getCount(int count) { final int length = this.activeViews.length; if (count > length) { final View[] activeViews = this.activeViews; this.activeViews = Arrays.copyOf(activeViews, count); // Log.e("getCount", "activeViews[" + (count - 1) + "]=" + this.activeViews[count-1]); } } /** 添加記錄當前活動的view */ public void addActiveView(int position, View view) { final int length = this.activeViews.length; if (position > length - 1) { getCount(position + 1); } this.activeViews[position] = view; // Log.e("addActiveView", "activeViews.size() = " + Arrays.toString(activeViews)); } /** 獲取某個活動view */ public View getActiveView(int position) { final int length = this.activeViews.length; if (position > length - 1) { getCount(position + 1); } return activeViews[position]; } /** 獲取廢棄的view */ View getRecycleView(int position) { return retrieveFromRecycle(recycleViews, position); } /** 檢索廢棄的view */ static View retrieveFromRecycle(SparseArray<View> recycleViews, int position) { int size = recycleViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i = 0; i < size; i++) { int fromPosition = recycleViews.keyAt(i); View view = recycleViews.get(fromPosition); if (fromPosition == position) { recycleViews.remove(fromPosition); return view; } } int index = size - 1; View r = recycleViews.valueAt(index); recycleViews.remove(recycleViews.keyAt(index)); return r; } else { return null; } } /** 添加廢棄的view,無序添加 */ void addRecycleView(int position, View scrap) { recycleViews.put(position, scrap); final int length = this.activeViews.length; if (position < length) { this.activeViews[position] = null; } pruneRecycleViews(); // Log.e("Recycle", "Recycle.size() = " +recycleViews.size()); } /** 將全部剩餘的活動view移到廢棄view裏 */ void recycleAllActiveViews() { final View[] activeViews = this.activeViews; SparseArray<View> recycleViews = this.recycleViews; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { activeViews[i] = null; recycleViews.put(i, victim); } } pruneRecycleViews(); } /** 確保廢棄的view很少於活動的view容器的總數量 */ private void pruneRecycleViews() { final int maxViews = activeViews.length; int size = recycleViews.size(); final int extras = size - maxViews; size--; for (int j = 0; j < extras; j++) { recycleViews.remove(recycleViews.keyAt(size--)); } } }
方法僅供參考,具體實現過程還需根據項目實際需求進行修改或優化。歡迎各路高手不吝賜教,多多交流!
參考資料: