標籤:

源碼解析之ListView

大家元旦快樂~

好記性不如爛筆頭,所以我準備弄個源碼解析系列,不準備詳細解析源碼,但把基本原理和設計思想梳理清楚,也給自己留個筆記存檔好在後面需要的時候翻起。

今天就從ListView開始。

ListView的核心在於layoutChildren函數,分兩種情況,一種是全新載入,第二種是非全新載入。主要區別在於ListView內部的View緩存池的使用,下面依次來講下。

在layoutChildren裡面,會根據LayoutMode選擇調用fillSpecific/fillUp/fillFromTop之類的函數來進行填充,這個只是策略問題不是很關鍵,最終這些函數都調用了makeAndAddView,然後再調用obtainView,再調用adapter.getView,拿到view之後再使用setupChild加入到ListView裡面去並放好位置(包含child自己的measure),流程如下:

setupChild函數裡面片段:

全新載入的很好理解,每個Item都是按照上面的流程走,並且每個Item的view都是通過getView裡面inflate出來的(這種情況getView的convertView傳過來是空,意味著ListView還沒有緩存view可以使用)

非全新載入,比如頁面滑動,或者adapter數據發生變化,這種情況下面整體流程和全新載入沒有區別,但在部分函數調用裡面有細微差別,比如:

layoutChildren裡面首先將ListView的child都放入緩存池:

// Pull all children into the RecycleBin.

// These views will be reused if possible

final int firstPosition = mFirstPosition;

final RecycleBin recycleBin = mRecycler;

if (dataChanged) {

for (int i = 0; i < childCount; i++) {

recycleBin.addScrapView(getChildAt(i), firstPosition+i);

}

} else {

recycleBin.fillActiveViews(childCount, firstPosition);

}

緩存池管理就是這個RecycleBin對象,他裡面有兩種緩存,一種叫ActiveViews,看上面代碼如果不是數據發生變化的非全新載入(比如頁面滾動),則把所有子view都放入ActiveViews,然後重新計算位置重新擺放新的view的時候,就會首先從ActiveViews裡面拿出緩存view,看makeAndAddView函數的第一段就是:

更巧妙的是,在拿ActiveView的緩存view的時候,會根據位置來拿,這樣的view認為是不需要重新經過adapter的getView函數的,這樣極大的提高了效率。(頁面滾動的時候頁面裡面的item只是位置變化,不需要重新調用adapter.getView函數)

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;

}

假如ActiveViews裡面拿不到緩存view了,比如ListView高度發生了變化,需要更多的view來填充,這個時候就會從另外一種緩存裡面拿,叫做ScrapViews。這種緩存view是屬於ListView被填滿了,結果還剩餘有view就會被放入到這個緩存池裡面來。在layoutChildren函數的尾部可以看到有這樣一段代碼就是這個意思:

// Flush any cached views that did not get reused above

recycleBin.scrapActiveViews();

/**

* Move all views remaining in mActiveViews to mScrapViews.

*/

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--) {

.......

從ScrapViews拿緩存view的代碼在obtainView裡面:

final View scrapView = mRecycler.getScrapView(position);

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.

mRecycler.addScrapView(scrapView, position);

} else if (child.isTemporarilyDetached()) {

outMetadata[0] = true;

// Finish the temporary detach started in addScrapView().

child.dispatchFinishTemporaryDetach();

}

}

可以看到adapter.getView的第二個參數convertView就是從ScrapViews裡面拿過來的緩存view,可能為空也可能不為空,所以我們的getView函數都要有null判斷。

這樣子我們就能串起來了,在makeAndAddView裡面先看看ActiveViews裡面有沒有緩存view,有的話則直接成功返回也不需要調用adapter.getView。沒有的話則繼續調用obtainView,然後再去ScrapViews裡面看看有沒有緩存view,ScrapViews裡面拿出來的view都需要重新經過adapter.getView進行重新刷數據。ScrapViews可能拿到緩存view也可能拿不到,所以getView實現需要null判斷。

我們可以把ActiveViews和ScrapViews理解為一二級緩存,有效率上面的差別(很明顯後者要經過getView重新綁定數據)

ListView的layoutChildren在開始的時候通過fillActiveViews將子view全部放到ActiveViews裡面,然後等會重新布局的時候又首先從ActiveViews裡面拿出來,不夠用的時候再繼續從ScrapViews裡面拿,非常高效。所以ActiveViews可以理解為layout期間的一個臨時產物。

整體流程就結束了,下面介紹下有些細節的地方可以從中學習到的。

View有onAttachedToWindow/onDetachFromWindow,還有一個不怎麼常用的onStartTemporaryDetach/onFinishTemporaryDetach,表示開始和結束臨時Detach,這種狀態View其實沒有真正觸發onDetachFromWindow,只是臨時的Detached了,在addScrapView函數里看到有調用start,view被放入ScrapViews緩存池了,臨時被detach:

然後在obtainView裡面,從ScrapViews裡面重新拿出來要使用了,再調用finish:

這個臨時detached挺有意思,結合ViewGroup的detachAllViewsFromParent(在layoutChildren裡面一開始就會調用這個方法),讓子view的parent都設成null,可以達到一些很巧妙的實現。

ScrapViews裡面的緩存view有的是真正onDetachFromWindow,有的則是onStartTemporaryDetach,造成這個的原因主要是scrapActiveViews和addScrapView兩個實現上的差異,不過這個不影響實際使用,obtainView裡面會根據實際情況來決定拿到的緩存view是要重新attachToWindow還是finshTemporaryDetach,這個outMetData[0](和mIsScrap[0]是同一個)就是標記緩存view是不是之前已經detachFromWiindow(之前是isTemporarilyDetached的,說明還未真正detachFromWindow)

然後setupChild裡面會根據是不是attachedToWindow做不同的操作:重新指定parent(attachViewToParent)還是重新attachToWindow(addViewInLayout)

源碼解析就怕別人看不懂,但願對同學們有些幫助~

更多文章關注微信公眾號:安卓之美

推薦閱讀:

RecyclerView 必知必會

TAG:ListView | Android |