Android ListView工做原理徹底解析(轉自 郭霖老師博客)

原文地址:http://blog.csdn.net/guolin_blog/article/details/44996879java

 

Android全部經常使用的原生控件當中,用法最複雜的應該就是ListView了,它專門用於處理那種內容元素不少,手機屏幕沒法展現出全部內容的狀況。ListView可使用列表的形式來展現內容,超出屏幕部分的內容只須要經過手指滑動就能夠移動到屏幕內了。mysql

 

另外ListView還有一個很是神奇的功能,我相信你們應該都體驗過,即便在ListView中加載很是很是多的數據,好比達到成百上千條甚至更多,ListView都不會發生OOM或者崩潰,並且隨着咱們手指滑動來瀏覽更多數據時,程序所佔用的內存居然都不會跟着增加。那麼ListView是怎麼實現這麼神奇的功能的呢?當初我就抱着學習的心態花了很長時間把ListView的源碼通讀了一遍,基本瞭解了它的工做原理,在感嘆Google大神可以寫出如此精妙代碼的同時我也有所敬畏,由於ListView的代碼量比較大,複雜度也很高,很難用文字表達清楚,因而我就放棄了把它寫成一篇博客的想法。那麼如今回想起來這件事我已經腸子都悔青了,由於沒過幾個月時間我就把當初梳理清晰的源碼又忘的一乾二淨。因而如今我又從新定下心來再次把ListView的源碼重讀了一遍,那麼此次我必定要把它寫成一篇博客,分享給你們的同時也當成我本身的筆記吧。android

首先咱們先來看一下ListView的繼承結構,以下圖所示:算法

 

能夠看到,ListView的繼承結構仍是至關複雜的,它是直接繼承自的AbsListView,而AbsListView有兩個子實現類,一個是ListView,另外一個就是GridView,所以咱們從這一點就能夠猜出來,ListView和GridView在工做原理和實現上都是有不少共同點的。而後AbsListView又繼承自AdapterView,AdapterView繼承自ViewGroup,後面就是咱們所熟知的了。先把ListView的繼承結構瞭解一下,待會兒有助於咱們更加清晰地分析代碼。sql

 

 

Adapter的做用

 

 

Adapter相信你們都不會陌生,咱們平時使用ListView的時候必定都會用到它。那麼話說回來你們有沒有仔細想過,爲何須要Adapter這個東西呢?總感受正由於有了Adapter,ListView的使用變得要比其它控件複雜得多。那麼這裏咱們就先來學習一下Adapter到底起到了什麼樣的一個做用。數據庫

 

其實說到底,控件就是爲了交互和展現數據用的,只不過ListView更加特殊,它是爲了展現不少不少數據用的,可是ListView只承擔交互和展現工做而已,至於這些數據來自哪裏,ListView是不關心的。所以,咱們能設想到的最基本的ListView工做模式就是要有一個ListView控件和一個數據源。數組

 

不過若是真的讓ListView和數據源直接打交道的話,那ListView所要作的適配工做就很是繁雜了。由於數據源這個概念太模糊了,咱們只知道它包含了不少數據而已,至於這個數據源究竟是什麼樣類型,並無嚴格的定義,有多是數組,也有多是集合,甚至有多是數據庫表中查詢出來的遊標。因此說若是ListView真的去爲每一種數據源都進行適配操做的話,一是擴展性會比較差,內置了幾種適配就只有幾種適配,不能動態進行添加。二是超出了它自己應該負責的工做範圍,再也不是僅僅承擔交互和展現工做就能夠了,這樣ListView就會變得比較臃腫。緩存

 

那麼顯然android開發團隊是不會容許這種事情發生的,因而就有了Adapter這樣一個機制的出現。顧名思義,Adapter是適配器的意思,它在ListView和數據源之間起到了一個橋樑的做用,ListView並不會直接和數據源打交道,而是會藉助Adapter這個橋樑來去訪問真正的數據源,與以前不一樣的是,Adapter的接口都是統一的,所以ListView不用再去擔憂任何適配方面的問題。而Adapter又是一個接口(interface),它能夠去實現各類各樣的子類,每一個子類都能經過本身的邏輯來去完成特定的功能,以及與特定數據源的適配操做,好比說ArrayAdapter能夠用於數組和List類型的數據源適配,SimpleCursorAdapter能夠用於遊標類型的數據源適配,這樣就很是巧妙地把數據源適配困難的問題解決掉了,而且還擁有至關不錯的擴展性。簡單的原理示意圖以下所示:佈局

固然Adapter的做用不只僅只有數據源適配這一點,還有一個很是很是重要的方法也須要咱們在Adapter當中去重寫,就是getView()方法,這個在下面的文章中還會詳細講到。學習

 

RecycleBin機制

 

那麼在開始分析ListView的源碼以前,還有一個東西是咱們提早須要瞭解的,就是RecycleBin機制,這個機制也是ListView可以實現成百上千條數據都不會OOM最重要的一個緣由。其實RecycleBin的代碼並很少,只有300行左右,它是寫在AbsListView中的一個內部類,因此全部繼承自AbsListView的子類,也就是ListView和GridView,均可以使用這個機制。那咱們來看一下RecycleBin中的主要代碼,以下所示:

/**
 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
 * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
 * those views which were onscreen at the start of a layout. By
 * construction, they are displaying current information. At the end of
 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
 * are old views that could potentially be used by the adapter to avoid
 * allocating views unnecessarily.
 * 
 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
 * @see android.widget.AbsListView.RecyclerListener
 */
class RecycleBin {
	private RecyclerListener mRecyclerListener;

	/**
	 * The position of the first view stored in mActiveViews.
	 */
	private int mFirstActivePosition;

	/**
	 * Views that were on screen at the start of layout. This array is
	 * populated at the start of layout, and at the end of layout all view
	 * in mActiveViews are moved to mScrapViews. Views in mActiveViews
	 * represent a contiguous range of Views, with position of the first
	 * view store in mFirstActivePosition.
	 */
	private View[] mActiveViews = new View[0];

	/**
	 * Unsorted views that can be used by the adapter as a convert view.
	 */
	private ArrayList<View>[] mScrapViews;

	private int mViewTypeCount;

	private ArrayList<View> mCurrentScrap;

	/**
	 * Fill ActiveViews with all of the children of the AbsListView.
	 * 
	 * @param childCount
	 *            The minimum number of views mActiveViews should hold
	 * @param firstActivePosition
	 *            The position of the first view that will be stored in
	 *            mActiveViews
	 */
	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;
			}
		}
	}

	/**
	 * Get the view corresponding to the specified position. The view will
	 * be removed from mActiveViews if it is found.
	 * 
	 * @param position
	 *            The position to look up in mActiveViews
	 * @return The view if it is found, null otherwise
	 */
	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;
	}

	/**
	 * Put a view into the ScapViews list. These views are unordered.
	 * 
	 * @param scrap
	 *            The view to add
	 */
	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);
		}
	}

	/**
	 * @return A view from the ScrapViews collection. These are unordered.
	 */
	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;
	}

	public void setViewTypeCount(int viewTypeCount) {
		if (viewTypeCount < 1) {
			throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
		}
		// noinspection unchecked
		ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
		for (int i = 0; i < viewTypeCount; i++) {
			scrapViews[i] = new ArrayList<View>();
		}
		mViewTypeCount = viewTypeCount;
		mCurrentScrap = scrapViews[0];
		mScrapViews = scrapViews;
	}

}

 

這裏的RecycleBin代碼並不全,我只是把最主要的幾個方法提了出來。那麼咱們先來對這幾個方法進行簡單解讀,這對後面分析ListView的工做原理將會有很大的幫助。

  • fillActiveViews() 這個方法接收兩個參數,第一個參數表示要存儲的view的數量,第二個參數表示ListView中第一個可見元素的position值。RecycleBin當中使用mActiveViews這個數組來存儲View,調用這個方法後就會根據傳入的參數來將ListView中的指定元素存儲到mActiveViews數組當中。
  • getActiveView() 這個方法和fillActiveViews()是對應的,用於從mActiveViews數組當中獲取數據。該方法接收一個position參數,表示元素在ListView當中的位置,方法內部會自動將position值轉換成mActiveViews數組對應的下標值。須要注意的是,mActiveViews當中所存儲的View,一旦被獲取了以後就會從mActiveViews當中移除,下次獲取一樣位置的View將會返回null,也就是說mActiveViews不能被重複利用。
  • addScrapView() 用於將一個廢棄的View進行緩存,該方法接收一個View參數,當有某個View肯定要廢棄掉的時候(好比滾動出了屏幕),就應該調用這個方法來對View進行緩存,RecycleBin當中使用mScrapViews和mCurrentScrap這兩個List來存儲廢棄View。
  • getScrapView 用於從廢棄緩存中取出一個View,這些廢棄緩存中的View是沒有順序可言的,所以getScrapView()方法中的算法也很是簡單,就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回。
  • setViewTypeCount() 咱們都知道Adapter當中能夠重寫一個getViewTypeCount()來表示ListView中有幾種類型的數據項,而setViewTypeCount()方法的做用就是爲每種類型的數據項都單獨啓用一個RecycleBin緩存機制。實際上,getViewTypeCount()方法一般狀況下使用的並非不少,因此咱們只要知道RecycleBin當中有這樣一個功能就好了。

 

瞭解了RecycleBin中的主要方法以及它們的用處以後,下面就能夠開始來分析ListView的工做原理了,這裏我將仍是按照之前分析源碼的方式來進行,即跟着主線執行流程來逐步閱讀並點到即止,否則的話要是把ListView全部的代碼都貼出來,那麼本篇文章將會很長很長了。

 

第一次Layout

 

無論怎麼說,ListView即便再特殊最終仍是繼承自View的,所以它的執行流程還將會按照View的規則來執行,對於這方面不太熟悉的朋友能夠參考我以前寫的 Android視圖繪製流程徹底解析,帶你一步步深刻了解View(二) 。

 

View的執行流程無非就分爲三步,onMeasure()用於測量View的大小,onLayout()用於肯定View的佈局,onDraw()用於將View繪製到界面上。而在ListView當中,onMeasure()並無什麼特殊的地方,由於它終歸是一個View,佔用的空間最多而且一般也就是整個屏幕。onDraw()在ListView當中也沒有什麼意義,由於ListView自己並不負責繪製,而是由ListView當中的子元素來進行繪製的。那麼ListView大部分的神奇功能其實都是在onLayout()方法中進行的了,所以咱們本篇文章也是主要分析的這個方法裏的內容。

 

若是你到ListView源碼中去找一找,你會發現ListView中是沒有onLayout()這個方法的,這是由於這個方法是在ListView的父類AbsListView中實現的,代碼以下所示:

相關文章
相關標籤/搜索