Android常考問題(5)-ListView及其複用

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()的開銷)。優化三:滑動不載入圖片(個人感覺不太好)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章