Listview原始碼分析
本文主要內容
Listview是一種常用的控制元件,它的主要特點是能夠複用,上下滑動時不至於卡頓,記憶體波動等。要實現這種功能,肯定存在著快取機制,今天我們著重分析下Listview的快取機制以及它的設計模式。
- 1、Listview簡介
- 2、快取分析
- 3、Listview原理
- 4、滑動原理
- 5、總結
1、Listview簡介
Listview的繼承關係如下,它的父類是AbsListView,快取類是在AbsListView中實現的。

如果是我們來設計Listview,我們會如何設計?
Listview可以上下滑動,子view被滑動出螢幕以後要如何處理?如果直接將子view刪除回收,那麼每次滑動都要重新inflate子view的佈局,將會導致效能問題,而且記憶體回收也會更頻繁,導致記憶體波動較大,因為記憶體並不一定會這麼及時地釋放。
所以需要快取機制,將滑動出螢幕以外的子view快取起來,為後續重用做準備,這樣效能問題不存在了,記憶體和效能相互平衡。
另外,Listview理論上可以顯示任意形式的子view,為了方便開發者,也為了解耦資料來源,封裝了adapter,這是絕佳的介面卡模式示例了。
2、快取分析
Listview快取依賴於RecycleBin類(AbsListView的內部類)。一起來看看RecycleBin類的邏輯:
class RecycleBin { //mActiveViews中第一個view在Listview中的位置 private int mFirstActivePosition; //快取當前正在顯示的view private View[] mActiveViews = new View[0]; //快取沒有被顯示的view,廢棄view,注意它是一個List陣列 private ArrayList<View>[] mScrapViews; //型別值,有多少種類型,mScrapViews陣列的長度就是多少 private int mViewTypeCount; //當前的廢棄view快取列表 private ArrayList<View> mCurrentScrap; //設定資料型別,有幾種型別就有多少種廢棄view快取列表,一種型別對應一個列表 public void setViewTypeCount(int viewTypeCount) { 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; } //將正在顯示的子view都新增到mActiveViews陣列中來 void fillActiveViews(int childCount, int firstActivePosition) { 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; } } } //獲取某個位置上的正在顯示的view 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; } //新增廢棄view,注意如果子view是暫存狀態,即hasTransientState為true,則不快取 void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); lp.scrappedFromPosition = position; final int viewType = lp.viewType; if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } } //將所有正在顯示的view都新增到廢棄快取列表中 void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList<View> scrapViews = mCurrentScrap; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams(); final int whichScrap = lp.viewType; activeViews[i] = null; lp.scrappedFromPosition = mFirstActivePosition + i; scrapViews.add(victim); } } //刪除過多的廢棄快取 pruneScrapViews(); } //刪除過多的廢棄快取,因為快取也不是無上限的,確定廢棄快取不超過mActiveViews陣列的長度 private void pruneScrapViews() { final int maxViews = mActiveViews.length; final int viewTypeCount = mViewTypeCount; final ArrayList<View>[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList<View> scrapPile = scrapViews[i]; int size = scrapPile.size(); final int extras = size - maxViews; size--; for (int j = 0; j < extras; j++) { removeDetachedView(scrapPile.remove(size--), false); } } } //從廢棄快取列表中取一個子view出來,注意有不同的取法,某至有匹配不到直接拿廢棄列表中的最後一個 //所以adapter的getView方法中,需要對converView做檢查,匹配,否則有可能出現數據錯亂 private View retrieveFromScrap(ArrayList<View> scrapViews, int position) { final int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position or ID. for (int i = 0; i < size; i++) { final View view = scrapViews.get(i); final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams(); if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { final View scrap = scrapViews.remove(i); clearAccessibilityFromScrap(scrap); return scrap; } } final View scrap = scrapViews.remove(size - 1); clearAccessibilityFromScrap(scrap); return scrap; } else { return null; } } }
需要注意幾點:
-
如果子view處理TransientState狀態,即hasTransientState方法返回為true,子view不會被新增到廢棄列表中
-
二級快取,第一重快取為mActiveViews,代表著當前正在顯示的view,第二重快取為廢棄view佇列陣列。它們之間的資料流轉,需要檢視Listview原始碼才能解釋清楚
-
注意到廢棄view的快取是一個列表陣列,而mActiveViews則只是一個數組。理論上廢棄view快取是一個數組就行了,為什麼現在有多個廢棄view隊列了呢?這是因為Listview可能有多種資料型別,一種資料型別對應著一個廢棄view佇列。在Listview或者RecycleView中顯示多種資料型別,相信很多人都用過吧。
-
只要是快取,肯定有對應的快取演算法,當要快取數量的數量超過了最大值了怎麼辦?要刪除哪些快取才能裝得下新快取。熟悉的有LRU演算法(最近最少使用演算法),而Listview比較簡單,檢視 pruneScrapViews 方法,只要廢棄佇列的長度超過了 mActiveViews 的長度,就從廢棄佇列最末尾開始刪除,直到不超過為止。每次新增新項到廢棄佇列中,都會檢查。
3、Listview原理
任何一個控制元件的原理都脫不開基本的View原理,即measure、layout、draw這一套,所以就根據這個原理,盤它!!
measure用於確定自身以及子view的大小,Listview的measure過程比較簡單,自身大小受限於Listview父控制元件,所以measure過程不改變它。而子view的大小也根據使用者的設定即可。
layout是整個過程中最複雜的一項,程式碼非常的長。onLayout方法中定義在父類AbsListView中,AbsListView還定義了一個抽象方法,layoutChildren,它的子類可以根據自身需求更改佈局。別忘了,AbsListView還有個子類,GridView。這種設計思想值得學習,模板方法,鉤子函式。
protected void layoutChildren() { final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; //如果資料改變,那麼將所有子view都新增到廢棄列表中 if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { //如果資料不變,就將當前正在顯示的所有view新增到mActiveViews中 recycleBin.fillActiveViews(childCount, firstPosition); } // 刪除所有的子view,要重新新增 detachAllViewsFromParent(); switch (mLayoutMode) { default: //根據Listview的模式,確定填充子view的方法,一般情況下,是Normal模式,會走下邊這個方法 fillFromTop if (!mStackFromBottom) { final int position = lookForSelectablePosition(0, true); setSelectedPositionInt(position); sel = fillFromTop(childrenTop); } } // Flush any cached views that did not get reused above //在fillFromTop方法中,獲取子view的時候其實就會從mActiveViews中拿快取資料,如果fillFromTop方法執行完了, // mActiveViews 中還有資料,則將剩餘的資料新增到廢棄佇列中 //為什麼 mActiveViews 中的資料沒有被 fillFromTop 全被拿完呢,理論上 Listview不動,mActiveViews肯定會被拿完的 //在使用者滑動列表後,mActiveViews 有可能沒有被拿完 recycleBin.scrapActiveViews(); }
接著我們來看 fillFromTop 方法
private View fillDown(int pos, int nextTop) { View selectedView = null; //end代表著整個listview的高度 int end = (mBottom - mTop); //nexttop表示listview的top位置 while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); //nextTop的值隨著子view位置的往下,而越來越大,當nextTop值大於或等於end值時,跳出迴圈,不再新增子view //所有listview上只在可視區域新增子view。非可視區域並沒有子view存在 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; } return selectedView; }
再看看makeAndAddView方法:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // Try to use an existing view for this position //如果沒有資料改變,直接先從 Active 快取中拿資料,拿到了就呼叫setupChild方法,將子view新增到listview中來 child = mRecycler.getActiveView(position); if (child != null) { setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible //Active快取中沒有,從廢棄快取中拿資料,然後呼叫adapter的getView方法,生成一個子view,將子view新增到listview上 child = obtainView(position, mIsScrap); setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
setupChild 方法就不再講述了,它的重點在於呼叫子view的layout方法,並且將子view新增到listview中來。
而obtainView則是獲取新的子view的,它會呼叫adapter的getView方法,獲取子view:
//注意trace操作,在抓systrace時,如果有listview,就可以看到obtainView方法執行的細節 View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; //從廢棄佇列中拿快取資料 final View scrapView = mRecycler.getScrapView(position); //呼叫adapter的getView方法,給了開發一個重用的機會,不用重新inflate view了,提高了效率 final View child = mAdapter.getView(position, scrapView, this); if (scrapView != null) { if (child != scrapView) { // Failed to re-bind the data, return scrap to the heap. //rebind過程,如果快取和child不一樣,重新放回廢棄佇列 mRecycler.addScrapView(scrapView, position); } } //設定layout引數 setItemViewLayoutParams(child, position); Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }
到此為止,layout流程已經分析完了,快取間的資料流轉也明瞭了。在layout之前,將所有的顯示的子view新增到active快取當中。然後刪除所有的子view,重新載入。在重新載入過程中,會先到active快取中查詢資料,如果有就直接新增子view,如果沒有再去廢棄快取中查詢。最後子view新增完後,將active快取中剩下的view新增到廢棄快取中。每次新增廢棄快取時,都要去檢查廢棄快取的容量,如果超過了active快取的長度,則從隊尾開始刪除,直接少於為止。

4、滑動原理
Listview在layout過程中,只會在可見區域新增子view,可見區域外並不會新增,而滑動過程中使用者能夠看到新的子view,那麼滑動時肯定動態新增子view了。另外快取資料肯定也有一些操作。帶著這些猜想一起來看吧。
滑動的流程稍顯複雜,先來一張流程圖:

猜想都在 trackMotionScroll 這個方法中,刪除不可見子view並快取起來,然後新增新的子view,最後再將子view的位置也移動,盡在於此。
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { //判定滑動方向,如果 incrementalDeltaY 小於0,則是向下滑動 final boolean down = incrementalDeltaY < 0; int start = 0; int count = 0; if (down) { int top = -incrementalDeltaY; for (int i = 0; i < childCount; i++) { //向下滑動,子view的bottom值大於listview的top值,則表示子view仍居於可視位置,那麼這類子view略過不處理 final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { //將不可見view新增到廢棄快取中 count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child, position); } } } } else { //另一種情況,往上滑動 int bottom = getHeight() - incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { bottom -= listPadding.bottom; } for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child, position); } } } } //count代表要刪除的子view,如果count大於0,則從start開始,刪除count個子view,這些子view已經不可見了 if (count > 0) { detachViewsFromParent(start, count); mRecycler.removeSkippedScrap(); } //滑動剩餘子view,讓子view也跟著手指動起來 offsetChildrenTopAndBottom(incrementalDeltaY); //有些子view不可見了,必然有空位出來,空位要新增新的子view,fillGap,聽名字就是填充子view的方法 if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } }
讀完程式碼後,上面判斷down和up是應該相反的方法,如果是向上滑動,則是走down的流程,向下則是up的流程。
在向上滑動過程中,從第一個子view開始計算,如果子view的bottom位置小於listview的top位置,顯然這個子view已經不可見了,要被新增到廢棄快取中去。最後記錄起點,從起點開始刪除count個子view。再執行 offsetChildrenTopAndBottom 方法,移動仍然可見的子view。
public void offsetChildrenTopAndBottom(int offset) { final int count = mChildrenCount; final View[] children = mChildren; boolean invalidate = false; for (int i = 0; i < count; i++) { final View v = children[i]; v.mTop += offset; v.mBottom += offset; if (v.mRenderNode != null) { invalidate = true; v.mRenderNode.offsetTopAndBottom(offset); } } if (invalidate) { invalidateViewProperty(false, false); } notifySubtreeAccessibilityStateChangedIfNeeded(); }
從程式碼中可知,其實是遍歷所有可見子view,直接將子view的top值變化,這樣子view的位置當然就變了。於是listview可以跟手滑動了。
5、總結
Listview還有一處值得討論,就是它的adapter設計。Listview在設計之初,肯定是想能顯示一切型別的資料,它的子view可以是各種各樣的格式。既然子view的格式千差萬別,那麼如何來相容這些不同的資料,不同的格式。
adapter應運而生,將資料和view樣式放到adapter中讓使用者自己定義,這實在是天才的設計,這樣解耦了資料和listview的內在邏輯,listview得以專心處理自己的功能,而不用在意資料問題了,使用者也能有更大的定製空間了。
listview整體程式碼還是非常複雜,本文之後,獲益良多,多讀原始碼,越讀越會有新的收穫。