打開ListView源碼查看ListView的繼承結構,發現ListView是繼承自AbsListView
ListView作爲列表,可以顯示成百上千個item。如果有多少數據,就創建多少個item,會佔用很大內存,但是大多數item並沒有顯示在屏幕上,造成了內存浪費,所以移除屏幕的view就可以緩存起來,以便下次重用。ListView爲我們提供了convertView,來複用view,這裏介紹下ListView的緩存機制。
1. RecycleBin類
查看ListView源碼並沒有緩存的相關代碼,那一定是在父類中了。查看AbsListView的成員變量,有個mRecycler,看註釋:用來存儲不使用的view,其將被下次layout的時候重新使用,以避免創建新的實例。
/**
* 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是AbsListView的內部類。
RecycleBin:大意就是通過兩級緩存來緩存view。(RecycleBin在layout的過程中便於view重用,RecycleBin有兩級存儲:ActiveViews和ScrapViews。ActiveViews存儲的是layout開始的時候屏幕上那些view。layout結束後,所有ActiveViews中的view被移動到ScrapViews中。ScrapViews中的views是那些可能被adapter重新用到的view,以避免重新創建不必要的view。)
1.1 RecycleBin成員變量
貼出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.
*/
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;
private ArrayList<View> mSkippedScrap;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
}
mActiveViews:一級緩存,顧名思義活動等view,這些view是佈局過程開始屏幕上的view。layout開始時這個數組被填充,layout結束,mActiveViews中的view移動到mScrapViews。mActiveViews代表了一個連續範圍的views,其第一個view的位置存儲在mFirstActivePosition變量中。
mScrapViews :二級緩存,顧名思義廢棄的view,無序的被adapter的convertView使用的view的集合
mScrapViews是多個list組成的數組,數組的長度爲viewTypeCount,每個item是個list,所以每個list緩存不同類型item佈局的view,所以mScrapViews應該是下圖的樣子。
mCurrentScrap:是個List,當ListView的item佈局只有一種的時候使用該變量緩存view。所以mCurrentScrap是個簡化的mScrapViews。
1.2 RecycleBin的方法
看下RecycleBin與上述變量有關的方法,總體是對兩個變量內元素添加移動刪除等操作:(請對照源碼閱讀)
void fillActiveViews(int childCount, int firstActivePosition) ListView中的子view緩存到mActiveViews中
用ListView的所有子view填充ActiveViews,其中childCount是mActiveViews應該保存的最少的view數,firstActivePosition是mActiveViews中存儲的首個view的位置。從代碼看該方法的處理邏輯爲將當前ListView的0-childCount個view中的非header、footer添加到mActiveViews數組中。當Adapter中的數據個數未發生變化時,此時用戶可能只是滾動,或點擊等操作,ListView中item的個數會發生變化,因此,需要將可視的item加入到mActiveView中來管理。
View getActiveView(int position) 獲取一個mActiveViews
獲取mActiveViews中指定位置的view,如果找到會將該view從mActiveViews中移除。position是adapter中的絕對下標值,mFirstActivePosition前面說過了,是當前可視區域的下標值,對應在adapter中的絕對值,如果找到,則返回找到的View,並將mActiveView對應的位置設置爲null。
void scrapActiveViews()
將mActiveViews 中剩餘的view放入mScrapViews。實際上就是將mActiveView中未使用的view回收(因爲,此時已經移出可視區域了)。會調用mRecyclerListener.onMovedToScrapHeap(scrap);
private void pruneScrapViews() 確保mScrapViews 的數目不會超過mActiveViews的數目
(This can happen if an adapter does not recycle its views)。mScrapView中每個ScrapView數組大小不應該超過mActiveView的大小,如果超過,系統認爲程序並沒有複用convertView,而是每次都是創建一個新的view,爲了避免產生大量的閒置內存且增加OOM的風險,系統會在每次回收後,去檢查一下,將超過的部分釋放掉,節約內存降低OOM風險。
public void setViewTypeCount(int viewTypeCount)
設置子view的類型數目,這裏會初始化mScrapViews數組
public void markChildrenDirty()
爲每個子類調用forceLayout()。將mScrapView中回收回來的View設置一樣標誌,在下次被複用到ListView中時,告訴viewroot重新layout該view。forceLayout()方法只是設置標誌,並不會通知其parent來重新layout。
void clear()
清空廢棄view堆,並將這些View從窗口中分離。
View getScrapView(int position)
從廢棄堆中獲取一個view,並從堆中移除
void reclaimScrapViews(List<View> views)
將mScrapView中所有的緩存view全部添加到指定的view
list中
2.ListView的緩存原理
2.1 從ListView繪製說起
看ListView的繪製過程代碼,主要看layout的過程,ListView沒有實現onLayout,因爲AbsListView中實現了onLayout,這個方法調用了一個layoutChildre方法,看註釋。
/**
* Subclasses must override this method to layout their children.
*/
protected void layoutChildren() {
}
子類必須重寫該方法來layout它們的子元素。所以ListView實現了layoutChildren方法。該方法較長,刪除了些代碼,由於分支較多,只分析第一次layout的路徑,留下了與RecycleBin有關的些代碼。重要看有註釋位置:
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
......
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
//如果數據發生變化,把ListView的子view緩存到mScrapView中
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
//數據沒有變化緩存到mActiveViews中
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
//清空分離listView中的view
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
......
switch (mLayoutMode) {
......
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
//mLayoutMode默認爲LAYOUT_NORMAL,第一次執行到這裏
//該方法向下調用到makeAndAddView方法中
//數據沒有變化最終會mRecycler.getActiveView(position)
//數據有變化最後會調用,mRecycler.getScrapView(position)或者adapter.getView()獲取一個view
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
//把mActiveViews中剩餘的view移動到ScrapViews中
recycleBin.scrapActiveViews();
......
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
從源碼看繪製是下邊的樣子:
由於父ViewGroup的原因可能導致ListView layout多次
第一次由於ListView裏沒有item了所以與RecycleBin無關,最後調用adapter.getView獲得要layout的view。
第二次之後在layout的時候就是執行以下步驟,使用緩存的時候可能執行下邊兩個分支。
在makeAndAddView方法(重要方法)中,顧名思義就是make一個view添加到list上去。怎麼make呢,有三種情況:一是從mActiveViews中獲取,二是mAdapter中獲取,三是從mScrapViews中獲取了,最後在調用setupChild方法把make的view添加到ListView上去。
layoutChildren中有個mDataChanged,從單詞的意思我們就可以瞭解,這裏的優化規則就是基於數據是否有變化,(當Adapter中的數據個數未發生變化時,此時用戶可能只是滾動,或點擊等操作,ListView中item的個數會發生變化,因此,需要將可視的item加入到mActiveView中來管理。)
當爲false,走分支1用mActiveViews緩存:
mDataChange爲true走分支2,使用mScrapViews緩存
2.2 ListView滾動
ListView滾動的時候如何緩存view呢
當滑動的時候添加view,緩存view的操作是在ListView的scrollListItemsBy方法中做的,該方法邏輯比較清晰,就是向上滑動與向下滑動的邏輯處理,amount爲偏移量。
/**
* Scroll the children by amount, adding a view at the end and removing
* views that fall off as necessary.
*
* @param amount The amount (positive or negative) to scroll.
*/
private void scrollListItemsBy(int amount) {
//按指定像素垂直方向上移動所有子view
offsetChildrenTopAndBottom(amount);
final int listBottom = getHeight() - mListPadding.bottom;
final int listTop = mListPadding.top;
final AbsListView.RecycleBin recycleBin = mRecycler;
if (amount < 0) {//向上滑動
// shifted items up
// may need to pan views into the bottom space
int numChildren = getChildCount();
View last = getChildAt(numChildren - 1);
//當最下邊的一個人view的底部縱座標小於ListView的底部縱座標的時候 需要在ListView底部添加一個view
while (last.getBottom() < listBottom) {
final int lastVisiblePosition = mFirstPosition + numChildren - 1;
if (lastVisiblePosition < mItemCount - 1) {
last = addViewBelow(last, lastVisiblePosition);
numChildren++;
} else {
break;
}
}
// may have brought in the last child of the list that is skinnier
// than the fading edge, thereby leaving space at the end. need
// to shift back
//如果最後一個與ListView最下邊有空隙,移動所有子view向下
if (last.getBottom() < listBottom) {
offsetChildrenTopAndBottom(listBottom - last.getBottom());
}
// top views may be panned off screen
//最上邊的view可能移除了屏幕 添加到scrapView中
View first = getChildAt(0);
while (first.getBottom() < listTop) {
AbsListView.LayoutParams layoutParams = (AbsListView.LayoutParams) first.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(first, mFirstPosition);
}
detachViewFromParent(first);
first = getChildAt(0);
mFirstPosition++;
}
} else {//向下滑動
// shifted items down
View first = getChildAt(0);
// may need to pan views into top
//可能需要在最上邊添加view
while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
first = addViewAbove(first, mFirstPosition);
mFirstPosition--;
}
// may have brought the very first child of the list in too far and
// need to shift it back
if (first.getTop() > listTop) {
offsetChildrenTopAndBottom(listTop - first.getTop());
}
int lastIndex = getChildCount() - 1;
View last = getChildAt(lastIndex);
// bottom view may be panned off screen
//最下邊的view可能需要緩存起來
while (last.getTop() > listBottom) {
AbsListView.LayoutParams layoutParams = (AbsListView.LayoutParams) last.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(last, mFirstPosition+lastIndex);
}
detachViewFromParent(last);
last = getChildAt(--lastIndex);
}
}
}
向上滑動的
向下滑動同理。
這只是一個類型的item,多類型item對於的item會存入對應的ScrapViews列表數組中。需要adapter實現getItemViewType,getViewTypeCount兩個方法,來判斷存入哪個列表。
看上述方法中的 addViewAbove,addViewBelow分別是在上邊下邊添加view的方法,看他是怎麼從scrapView獲得一個view的
private View addViewAbove(View theView, int position) {
int abovePosition = position - 1;
View view = obtainView(abovePosition, mIsScrap);
int edgeOfNewChild = theView.getTop() - mDividerHeight;
setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
false, mIsScrap[0]);
return view;
}
private View addViewBelow(View theView, int position) {
int belowPosition = position + 1;
View view = obtainView(belowPosition, mIsScrap);
int edgeOfNewChild = theView.getBottom() + mDividerHeight;
setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
false, mIsScrap[0]);
return view;
}
看他們內部都是調用obtainView方法獲取一個view
獲取一個view,使它顯示與位置關聯的數據。當這個方法被調用時,說明Recycle bin中的view已經不可用了,那麼,現在唯一的方法就是,convert一個老的view,或者構造一個新的view。
position: 要顯示的位置
isScrap: 是個boolean數組, 如果view從scrap heap獲取,isScrap [0]爲true,否則爲false。
View obtainView(int position, boolean[] isScrap) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
isScrap[0] = false;
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
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);
// If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
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;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
setItemViewLayoutParams(child, position);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}
其中有個mRecycler.getScrapView方法,看他的內部
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
對佈局是一個類型還是多類型做了不同處理,都調用了retrieveFromScrap方法。
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;
}
}
大意就是優先獲取相同Id的view,然後是獲取本來這個位置的view,最後就是獲取scrapView的最後一個view。至此滑動時候ListView如何緩存分析完了,其中還有一些細節有待分析比如transientView。初寫博客有錯誤歡迎大家指正。
參考:
Android6.0.1_r10源碼