我們都知道ListView的baseAdapter中,使用了一個view的緩存回收機(jī)制,我們經(jīng)常被告知會把不可見的view緩存起來,并且在新的view顯示時會重用之前回收的view,實(shí)際中在開發(fā)時會使用convertView去進(jìn)行相關(guān)處理。那么我們不禁會好奇,這個緩存回收機(jī)制到底是怎么實(shí)現(xiàn)的?我們不該僅僅會使用BaseAdapter重寫各個方法就夠了,我們需要往深處去挖點(diǎn)寶藏。今天我們就來看看listView和adapter在視圖回收和緩存環(huán)節(jié)是怎么做到的。
AbsListView
ListView是一個繼承自AbsListView的類,要想深入這一部分,我們需要看看AbsListView的源碼。AbsListView還是比較復(fù)雜的,但是我們可以在ListView的scrollListItemsBy,layoutChildren等方法中看到幾個叫mRecycler 和recycleBin的對象,看命名似乎是和視圖回收機(jī)制重用等有關(guān),mRecycler就是來自AbsListView的,我們可以繼續(xù)看下去。
/**
* The data set used to store unused views that should be reused during the next layout
* to avoid creating new ones
*/
final RecycleBin mRecycler = new RecycleBin();
涉及到這個功能,recycleBin是一個RecycleBin對象,RecycleBin是AbsListView的內(nèi)部類。那我們就來研究一下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.
*/
大意就是RecycleBin實(shí)現(xiàn)了布局中view的重用。RecycleBin有兩個層級的存儲。
- ActiveViews , 布局開始時要在屏幕中顯示的view
- ScrapViews, 布局結(jié)束后所有的ActiveViews就降級為ScrapViews。ScrapViews就是舊view,主要是可能被adapter為了避免不必要的視圖分配空間而重用。
好了,RecycleBin的結(jié)構(gòu)我們搞懂了,那么關(guān)于ListView的核心問題就變成了RecycleBin是怎么運(yùn)用ActiveViews和ScrapViews的了。換句話說就是ActiveViews和ScrapViews是怎么產(chǎn)生、怎么添加、怎么交換的?
嗯,繼續(xù)看源碼。
private View[] mActiveViews = new View[0];
private ArrayList<View>[] mScrapViews;
mActiveViews就是一個ActiveViews堆,可以看到mActiveViews是一個View數(shù)組。
mScrapViews是一個ScrapViews堆,是一個ArrayList<View>數(shù)組。這里為什么要這樣設(shè)計(jì)存儲?我們先賣個關(guān)子。
在研究這兩個不同層級的View堆前,我們先看看在ListView中怎么使用RecycleBin的。
- setAdapter方法中,mRecycler.clear();
- setAdapter方法中,設(shè)計(jì)到了viewType的操作,因?yàn)闀胁煌囊晥D結(jié)構(gòu),mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
- onMeasure方法中,
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
- makeAndAddView方法中,mRecycler.getActiveView(position);
- layoutChildren方法中,
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
……
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
其中在layoutChildren中的用法特別典型,我們具體來看一看。
以上的這段代碼可以大概看出一些邏輯思路:
- 先判斷數(shù)據(jù)是否有改變,如果改變了就將當(dāng)前的children加到ScrapViews中,否則加到ActiveViews中。
- removeSkippedScrap,把舊的view都刪掉。
- 最后將以上沒有被重用的緩存的view都回收掉。將當(dāng)前的ActiveVies 移動到 ScrapViews。
以上是我們通過這一段代碼的一個猜測分析,現(xiàn)在一步步看看源碼。
dataChanged是一個AdapterView的boolean變量。
其中ListView 繼承自AbsListView, AbsListView 繼承自AdapterView,AdapterView繼承自ViewGroup.
對dataChanged的賦值主要是在AdapterView中的內(nèi)部類AdapterDataSetObserver中進(jìn)行的。我們知道listView的adapter使用了觀察者模式。這個是怎么做到的?
我們先看看AdapterDataSetObserver的源碼:
class AdapterDataSetObserver extends DataSetObserver {
private Parcelable mInstanceState = null;
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
rememberSyncState();
}
checkFocus();
requestLayout();
}
@Override
public void onInvalidated() {
mDataChanged = true;
if (AdapterView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = AdapterView.this.onSaveInstanceState();
}
// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkFocus();
requestLayout();
}
public void clearSavedState() {
mInstanceState = null;
}
}
代碼很簡單,繼承自DataSetObserver,重寫了onChanged和onInvalidated兩個方法,對mDataChanged的操作都是在數(shù)據(jù)發(fā)生改變后將mDataChanged設(shè)為true,那么在哪里會變成false呢?AdapterView中已經(jīng)沒有了。我們還需要回到ListView中繼續(xù)看。
在ListView中搜索這個變量會發(fā)現(xiàn),將其變?yōu)閒alse還是同樣的在layoutChildren中,并且是完成了對mActiveViews和mScrapViews的各種操作之后才變?yōu)閒alse。
并且在makeAndView中使用了false時的值。
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
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
makeAndAddView能夠獲取一個view并且把它添加到child的list中,并且返回了這個child,這個child可以是新view,也可以是沒有使用過的view
convert過來的,或者說是從緩存中重用的view。
這當(dāng)中有一個getActiveView方法,就是我們在之前提到的RecycleBin的第四個用法,也是在mDataChanged為false時一個處理方法。
好了,對mDataChanged的分析和“尋找”先到這里,我們接著看看RecycleBin的用法。
既然是我們之前講過的處理流程,我們先看看最初的一個RecycleBin使用情況。
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.scrappedFromPosition = position;
......
//當(dāng)一個view有瞬態(tài)時不用被廢棄
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
// 如果adapter有穩(wěn)定的ids,那就能對相同的數(shù)據(jù)進(jìn)行view的重用
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<View>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
// 如果綁定的數(shù)據(jù)沒有改變,就能在舊位置重用view
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<View>();
}
mTransientStateViews.put(position, scrap);
} else {
// 其他情況只能移除view并且從頭來過
if (mSkippedScrap == null) {
mSkippedScrap = new ArrayList<View>();
}
mSkippedScrap.add(scrap);
}
}else{
if (mViewTypeCount == 1) {
//這里的mCurrentScrap就是mScrapViews[0]
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
我們可以看到,這里的邏輯還算很清晰,在關(guān)于view的重用的判斷時,涉及到一個概念還是需要解釋一下,就是view的瞬態(tài)。
View.hasTransientState()的代碼就不貼了,主要是幾個flag的運(yùn)算,雖然就一行,但是需要前后聯(lián)系,感興趣的朋友可以去看看源碼。
我們這里主要理解瞬態(tài),當(dāng)我們說一個view有瞬態(tài)時,我們指app無需再關(guān)心這個view的保存與恢復(fù),注釋指出一般用來播放動畫或者記錄選擇的位置等相似的行為。
當(dāng)一個view標(biāo)記位有瞬態(tài)時,在RecycleBin中,就有可能不會降級到ScrapView,而是mTransientStateViews或者mTransientStateViewsById將其保存起來,以便于
后來的重用。
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
在RecycleBin中涉及到瞬態(tài)的存儲結(jié)構(gòu)是上述代碼中的兩個,其實(shí)就是SparseArray,一個存的是position的key,一個存的是id。
這里介紹了mTransientStateViews的寫,我們再看看mTransientStateViews的讀。這里的讀也是讀的SparseArray,雖然功能類似于HashMap,但是讀數(shù)據(jù)時使用
的是valueAt方法。
View getTransientStateView(int position) {
if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
long id = mAdapter.getItemId(position);
View result = mTransientStateViewsById.get(id);
mTransientStateViewsById.remove(id);
return result;
}
if (mTransientStateViews != null) {
final int index = mTransientStateViews.indexOfKey(position);
if (index >= 0) {
View result = mTransientStateViews.valueAt(index);
mTransientStateViews.removeAt(index);
return result;
}
}
return null;
}
上面這份代碼就是用來獲取瞬時態(tài)的view的,首先從mTransientStateViewsById中讀取,如果沒有就從mTransientStateViews中讀取。
這個方法是屬于RecycleBin的,但是這個是在哪里調(diào)用的呢?答案是AbsListView的obtainView方法。
這里是一個比較關(guān)鍵的地方了。
View obtainView(int position, boolean[] isScrap) {
......
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
// 重新綁定數(shù)據(jù)失敗,就廢棄獲取到的view
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
......
return transientView;
}
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 {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
......
setItemViewLayoutParams(child, position);
......
return child;
}
閱讀代碼可以發(fā)現(xiàn),在obtainView中,最重要的一個方法就是調(diào)用了adapter的getView方法,這個方法也是我們平常重寫的方法,getView返回的是一個view。
而在對這個view的獲取過程,有一個很明顯的兩層處理,首先就是嘗試獲取我們之前分析介紹的transientView,也就是擁有瞬時態(tài)的view,通過transientView
以完成復(fù)用。如果這一步走的失敗了,也就是transientView為null時,或者說這個view并不擁有瞬時態(tài),那么就從ScrapView中獲取一個scrapView,這里可能要對
scrapView多做一些處理,我在上面都省略了,不影響邏輯大局,感興趣的朋友可以去看看源碼。最后返回的是scrapView。
getTransientStateView我們已經(jīng)介紹過了,現(xiàn)在來看看失敗之后的getScrapView。
View getScrapView(int position) {
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
}
return null;
}
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// 檢查對某一個position或者id是否還有一個對應(yīng)的view
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;
}
}
上面這份代碼還是寫的很清楚了,根據(jù)不同的viewType來進(jìn)行不同策略的取scrapView,但是實(shí)質(zhì)上都是走到了retrieveFromScrap。
思路還是比較清晰,但是需要注意的是在get到scrapView后,在scrapView堆中這個view就會被移除掉。以便以后不停的循環(huán)往復(fù)的重用。
以上的分析都是在layoutChildren中判斷數(shù)據(jù)改變后的過程,當(dāng)數(shù)據(jù)沒有改變時,會走到fillActiveViews方法。這個方法能夠?qū)bsListView的所有子view都
裝到activeViews中。代碼很簡單,就是一個for循環(huán),將listView的所有子view一一存到activeViews中。
接著我們最初的分析,這些都走完之后,會執(zhí)行recycleBin的removeSkippedScrap方法。還記得我們介紹的addScrapView方法嗎,當(dāng)一個view是有瞬時態(tài)的,但是
卻沒有一個保存到mTransientStateViewsById或者mTransientStateViews中時,會存在mSkippedScrap中。這是都會通過removeSkippedScrap全部清空。
在layoutChildren中,涉及到RecycleBin的最后還有一個方法就是scrapActiveViews。因?yàn)橐呀?jīng)完成了children的布局layout位置的擺放等,所以這個時候需要刷新
緩存,scrapActiveViews這部分代碼可能處理的過程有點(diǎn)多,但是最重要的一件事就是將現(xiàn)在mActiveViews還剩下的views都會移到mScrapViews中。
這個遷移過程也是和addScrapView的過程差不多。一開始是先對mActiveViews遍歷,每次保存當(dāng)前結(jié)點(diǎn)victim,并將mActiveViews[i]置空,然后對victim進(jìn)行遷移
操作,如果符合條件就加到mTransientStateViewsById或者mTransientStateViews中,否則就加到mScrapViews中。
好了,至此在addScrapView中對RecycleBin進(jìn)行的一系列操作就講完了,我們看看下一步是什么。
ListView的makeAndAddView方法。
makeAndAddView的代碼我們之前也貼過了,當(dāng)判斷數(shù)據(jù)沒有發(fā)生改變時,會走到RecycleBin的getActiveView方法。
那我們來看看getActiveView。
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;
}
getActiveView的代碼就很簡單了,根據(jù)丟過來的position計(jì)算出一個真實(shí)有效的index,然后從activeViews中獲取相應(yīng)的view。沒什么可講的。
ok,關(guān)于RecycleBin的幾個主要方法和執(zhí)行流程就介紹完了。
我們可以暫時回顧小結(jié)一下,實(shí)際上RecycleBin的主要結(jié)構(gòu)就是三個,一個是activeView堆,結(jié)構(gòu)是一個View數(shù)組,另一個是scrapView堆,結(jié)構(gòu)是一個
ArrayList<View>數(shù)組。還有一個是transientViews,結(jié)構(gòu)是SparseArray,主要通過Id或者position存取。
幾個結(jié)構(gòu)可以理解為層級不同,activeView比scrapView高一點(diǎn),當(dāng)觸發(fā)了某種條件或者機(jī)制后,child的view就會從activieView中移到transientViews或者scrapView中進(jìn)行緩存。
當(dāng)ListView需要obtainView時,會先從有瞬時態(tài)的sparseArray中獲取view,當(dāng)失敗時就會去scrapViews中獲取view。
當(dāng)然,這些過程又是和一個boolean變量mDataChanged進(jìn)行配合的,具體的過程在上面的源碼分析中已經(jīng)解釋過了,諸位可以回過去看看。
基本思路是在給子view布局時,如果數(shù)據(jù)沒有發(fā)生改變,就使用當(dāng)前已經(jīng)存在ActiveViews的view。
在obtainView時,如果發(fā)生了改變,就addScrapView.否則就fill with activeView..
再次說到addScrapView,由于這是一個比較重要的方法,這里小結(jié)時我們也來看看哪些地方調(diào)用了addScrapView.
我們可以在ListView源碼中搜索addScrapView看看。
- onMeasure
- measureHeightOfChildren , measure listView指定范圍的高度, 在onMeasure中調(diào)用
- layoutChildren
- scrollListItemsBy , 以一定child數(shù)目滑動List,需要將滑出的child刪掉,在最后添加view
其實(shí)前三個中用到的addScrapView我們之前也已經(jīng)都講到了,addScrapView的實(shí)現(xiàn)過程也不算復(fù)雜,主要是和activeView以及有瞬態(tài)的view的配合使用。
第四個我們接下來講一下。
既然了解了RecycleBin的緩存結(jié)構(gòu)和基本方法后,我們來實(shí)戰(zhàn)看看,在一個Listview滑動過程中,到底是怎么實(shí)現(xiàn)view的回收的吧。
現(xiàn)在考慮滑動一個ListView的情況,也就是scrollListItemsBy方法。
private void scrollListItemsBy(int amount) {
...
final AbsListView.RecycleBin recycleBin = mRecycler;
if (amount < 0) { //上滑
...
View last = getChildAt(numChildren - 1);
while (last.getBottom() < listBottom) {
final int lastVisiblePosition = mFirstPosition + numChildren - 1;
if (lastVisiblePosition < mItemCount - 1) {
last = addViewBelow(last, lastVisiblePosition);
numChildren++;
}
...
}
...
View first = getChildAt(0);
while (first.getBottom() < listTop) {
AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(first, mFirstPosition);
}
...
}
...
}else{ //往下滑
...
View last = getChildAt(lastIndex);
...
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(last, mFirstPosition + lastIndex);
}
...
}
}
可以看到,每次ListView的滑動事件要將一個view滑出屏幕時,會將頭部的或者尾部的(視方向而定)childView通過addScrapView緩存起來,
緩存的流程就是我們一開始分析的了,先保存有瞬時態(tài)的view,然后視情況存到scrapViews中。
等到每次obtainView時再依序從緩存的view中取出來。
這樣就完成了一個滑動的緩存與回收。
好了,關(guān)于ListView的回收機(jī)制到這里就講的差不多了,本質(zhì)上就是對AbsListView的內(nèi)部類RecycleBin的操作。
當(dāng)我們弄懂了這個機(jī)制,才能更好的思考更多的問題。
例如,最后我問大家一個問題:
根據(jù)我之前所講的,當(dāng)一個ListView的有若干個viewType, 當(dāng)滑出的view和滑入添加的view的type不一樣,比如說滑出了一個TextView的item,
滑入了一個ImageView的Item, 那這種情況下還能復(fù)用剛才的view嗎?