listView複用解析
1.ListView第一次載入
時序圖:
ofollow,noindex">https://www.processon.com/view/link/5bd7b047e4b0fef7882c2fda
ListView第一次載入介面.png
第一次載入時,因為沒有快取view,所以通過adapter的getItem來獲得要載入的view,ScrapView快取為空,所以convertView為空。
2.第二次Layout
即使是一個再簡單的View,在展示到介面上之前都會經歷至少兩次onMeasure()和兩次onLayout()的過程,這就意味著layoutChildren()過程會執行兩次。
與第二次不同的是,ListView已經有第一次載入的快取,直接讀取快取載入介面。

ListView第二次Layout.png
@Override protected void layoutChildren() { ... 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(); ... //選擇載入view方案 switch (mLayoutMode) { ... default: if (childCount == 0) { if (!mStackFromBottom) { final int position = lookForSelectablePosition(0, true); setSelectedPositionInt(position); 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; } } //將所有沒有被重用到的view從mActiveViews轉移到mScrapViews中去 recycleBin.scrapActiveViews(); }
- 當資料沒有變化時,呼叫fillActiveViews用來快取所有的子View,然後呼叫detachAllViewsFromParent來清空view,在後面呼叫makeAndAddView時呼叫mRecycler.getActiveView(position)來獲取快取。如果mActiveViews還有剩餘的view沒有複用,最後都移到ScrapViews中去。
- 當資料變化時,呼叫recycleBin.addScrapView(getChildAt(i), firstPosition+i);將view都存在mCurrentScrap中(mViewTypeCount預設為1,預設存mCurrentScrap,對不同的mViewTypeCount有mScrapViews陣列儲存)
在addScrapView方法中,如果view正在執行動畫操作,設定setHasTransientView(true),將把view新增入mTransientStateViews中。在obtainView中會重新複用。
- 第一次載入沒有資料所以忽略。
在選擇選擇載入view方案時
- 在第一次呼叫載入方法時,因childCount == 0且不位於底部,走的fillFromTop方法。
- 第二次呼叫方法時,childCount大於0,根據點選的item位置和顯示的第一個item位置來構造載入view的位置。
- 最終都是迴圈呼叫makeAndAddView來載入單個view來顯示介面。複用view中,getActiveView優先最高,其次是obtainView中的transientView,最後是scrapView。
3.滾動載入view
onTouchEvent ——》 onTouchMove -》 scrollIfNeeded - 》 trackMotionScroll -》 fillGap -》 fillDown/fillUp -》 makeAndAddView - 》 obtainView -》 getScrapView
這裡關注點主要在介面複用和載入上。
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { if (down) { //判斷是否向下滾動 int top = -incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { top += listPadding.top; } for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. child.clearAccessibilityFocus(); 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) { // The view will be rebound to new data, clear any // system-managed transient state. child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); } } } } if (count > 0) { detachViewsFromParent(start, count); mRecycler.removeSkippedScrap(); } offsetChildrenTopAndBottom(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } }
- 如果是下劃,頂部的item會先滑出螢幕,迴圈已載入的view判斷底部是否移出的螢幕,移出了就呼叫mRecycler.addScrapView加入快取中。
- 同理,上劃螢幕,底部的item如果移出的螢幕也加入快取。
- 將移出螢幕的view呼叫detachViewsFromParent移出,並清空SkippedScrap。
- fillGap 呼叫條件是,有新的view移入螢幕,需要載入了。fillGap就是實現新view載入的方法。而fillGap里根據滑動方向分別呼叫了 fillDown 和 fillUp ,這個我們在第二次layout已經很熟悉了。
View obtainView(int position, boolean[] isScrap) { ... final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); ... }
與第二次layout不同的是,這是滾動中載入view,ActiveView為空,而ScrapView有快取,所以呼叫Adapter.getView方法,ScrapView的快取view作為convertView引數傳入進去。
4. notifyDataSetChanged
public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); } DataSetObservable.java public void notifyChanged() { synchronized(mObservers) { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onChanged(); } } }
mDataSetObservable是在AbsListView中的AdapterDataSetObserver類中實現,它繼承自AdapterView<ListAdapter>.AdapterDataSetObserver
@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(); }
最後一行,呼叫requestLayout(),onLayout要被呼叫,開始重新佈局。
5.總結
- 作為AbsListView的內部類RecycleBin,view重用主要靠它實現。
- mActiveViews在資料沒有改變而進行重繪時儲存(例如多次重繪;呼叫notifyDataSetChanged而資料沒有改變等),不用多次呼叫adapter的getView;
呼叫RecycleBin的fillActiveViews將view快取到mActiveViews。
- mScrapViews/mCurrentScrap來快取已經移除螢幕的view(例如listview滾動;資料有改變時呼叫notifyDataSetChanged),作為adapter的getView中convertView的引數;
RecycleBin的addScrapView或直接mRecycler.addScrapView加入快取
- mTransientStateViews/mTransientStateViewsById來快取處於transient的view(例如動畫未執行結束,hasTransientState為true,設定了setHasTransientView(true),而沒有設定false)。
- 三者的優先順序依次,mActiveViews在makeAndAddView中呼叫,後面兩個在obtainView方法中被呼叫。