ListView雖然已經幾乎被RecycleView取代,但是其複用的核心思想還是很棒的,而且也經常在入門級面試中被提問。在看RecycleView的時候雲裏霧裏的,就先理清ListView。這次的起因是因爲RecycleView複用出現的嚴重bug。其實很久之前就搗鼓過一次ListView的複用問題,現在回過頭看看真的是不可思議,我當時是怎麼搗鼓出的多層嵌套的ListView的同時還解決了複用引發的問題?當年的我真厲害,連adapter都不知道是啥還弄了出來這些?。扯遠了。
總而言之,這次乾脆把兩塊的源碼一起學習了。從簡單點的ListView開始。
之前記錄過ListView的控件使用,adapter的相關內容也比較簡單,就不贅述了。
首先要知道ListView是個啥。android常常會出現OOM或者崩潰之類的情況。究其原因是因爲內存溢出了。在一個列表項中,可能有成百上千個列表項,如果全部放進內存,爲每一個列表項加載一個新的列表單元,內存喫不住,程序猿也喫不消啊。因此就有了ListView。它的主要目的就是不斷複用減少相同單元佔用的內存。ListView可以使用列表的形式來展示內容,超出屏幕部分的內容只需要通過手指滑動就可以移動到屏幕內了。
ListView最大的優點就在於複用過程。也就是RecycleBin。這個類裏面的幾個主要方法:
fillActiveViews(int childCount, int firstActivePosition)
第一個參數是view的數量,第二個參數是開始的view的位置。這個方法就用來將指定位置的元素放到ListView 中相應位置上。
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();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
// active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
}
}
}
getActiveView(int position)
這個方法和前一個是對應的,用於獲取position位置的元素,然後移除掉獲取過的元素。
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >= 0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}
addScrapView(View scrap)
當一個View確定要廢棄掉的時候(比如滾動出了屏幕),這個方法就把View拿過來緩存。
void addScrapView(View scrap) {
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
// Don't put header or footer views or views that should be ignored
// into the scrap heap
int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(scrap, false);
}
return;
}
if (mViewTypeCount == 1) {
dispatchFinishTemporaryDetach(scrap);
mCurrentScrap.add(scrap);
} else {
dispatchFinishTemporaryDetach(scrap);
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
getScrapView(int position)
就取出被廢棄的view,廢棄緩存中的View是沒有順序的,因此getScrapView()方法就直接獲取尾部的一個scrap view進行返回。
View getScrapView(int position) {
ArrayList<View> scrapViews;
if (mViewTypeCount == 1) {
scrapViews = mCurrentScrap;
int size = scrapViews.size();
if (size > 0) {
return scrapViews.remove(size - 1);
} else {
return null;
}
} else {
int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
scrapViews = mScrapViews[whichScrap];
int size = scrapViews.size();
if (size > 0) {
return scrapViews.remove(size - 1);
}
}
}
return null;
}
setViewTypeCount()
這個方法的作用就是爲每種類型的數據項都單獨啓用一個RecycleBin緩存機制。重寫後有機會解決複用問題。
這是複用部分類的代碼。接下來是ListView的繪製過程,這部分主要是佈局的時候有別於其他view,因此其onLayout()方法很關鍵。它的佈局方法繼承了父類AbsListView。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
}
邏輯上就是判斷如果ListView發生了變化,changed就變爲true,就強制重繪全部的子佈局。在下面有一個layoutChildren()方法,然後在ListView中實現了這個方法。在layoutChildren()方法中主要工作就是子佈局的繪製。然後就在setupChild()方法裏面自頂部向下繪製,等到佈局超出ListView的範圍的時候就主動停止佈局。因此不多解釋代碼了。最關鍵的一段代碼在其中的obtainView()方法中。
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
可以看到一開始是從getScrapView()方法中獲取一個View。在初始化整個ListView的時候肯定是沒有廢棄的view的,因此這裏會拿到空值。於是就會去調用mAdapter裏面的內容,也就是我們的adapter的getView()方法。
public View getView(int position, View convertView, ViewGroup parent){
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
}
我們的getView方法有三個參數:position,view和父控件。而在上面getView()方法的第二個參數是null,也就是沒有佈局,因此就調用LayoutInflater的inflate()方法加載一個佈局,然後返回view。這樣就說明第一次進入的時候每一個item都是初始化加載出來的。
接下來就是複用邏輯了
複用過程和之前區別不大,一個是加載的時候,會從當前位置開始加載,然後再加載它上面和下面的其他item,另一個就是不會再調用inflate方法再次佈局了。這次就會返回一個true到setupChild()方法裏面,告訴setupChild()方法這個view是已經被佈局過的了,於是將一個之前detach的View重新attach到ViewGroup上。
這樣複用過程就結束了。(算了,這次又沒說清楚,怕是又連自己都看不懂了)
在圖中會發現,下滑的時候元素0的item中把數據丟出去,裝入了元素6的數據,就成了複用的過程。
這時候,複用的問題就出現了。假設元素中有個checkBox,我勾選了元素0,然後下滑,結果會發現元素6被勾上了?因爲這裏用的是元素0的checkBox,所以元素6同時也被勾選了。這是比較常見的問題,還記得幾年前我就被這個搞懵了。解決方法有幾個:
1.重寫你的adapter,這是最麻煩的,但也是最實用最穩定的方法,把checkBox這種屬性全部都和viewHolder綁定在一起,而不是和view綁定在一起,這樣就能夠實現在更改數據的時候checkBox屬性隨之更改。一般用setTag()方法綁定。
2.永不復用。別笑,這真的是一個方法,因爲列表的item不多,乾脆不再複用,這樣基本上是靠損失性能換取bug的修復。當然,一般能不這麼幹還是別這麼做了,確實太有風險了。
3.多重背景。這種checkBox就不適合這個方法了,也就是比如有三種類型的item的時候就幹錯做三種view,每次在adapter裏判斷情況選擇不同的view來應對,另幾個就隱藏起來。
順便說一下Listview的優化方式:優化一:convertView的使用,主要優化加載佈局問題,從上面分析就知道,多了convertView就實現了複用的關鍵之一。優化二;viewHolder的使用,緩存控件實例。(主要是減少findViewById()的開銷)。優化三:滑動不載入圖片(個人感覺不太好)。