RecyclerView快取機制(回收去哪?)
這是RecyclerView
快取機制系列文章的第三篇,系列文章的目錄如下:
上一篇以列表滑動事件為起點沿著呼叫鏈一直往下尋找,驗證了“滑出螢幕的表項”會被回收。那它們被回收去哪裡了?沿著上一篇的呼叫鏈繼續往下探究:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { ... /** * Recycles views that went out of bounds after scrolling towards the end of the layout. * 當向列表尾部滾動時回收滾出螢幕的表項 * <p> * Checks both layout position and visible position to guarantee that the view is not visible. * * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} * @param dtThis can be used to add additional padding to the visible area. This is used *to detect children that will go out of bounds after scrolling, without *actually moving them.(該引數被用於檢測滾出螢幕的表項) */ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { ... // ignore padding, ViewGroup may not clip children. final int limit = dt; final int childCount = getChildCount(); if (mShouldReverseLayout) { for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here recycleChildren(recycler, childCount - 1, i); return; } } } else { //遍歷LinearLayoutManager的孩子找出其中應該被回收的 for (int i = 0; i < childCount; i++) { View child = getChildAt(i); //直到表項底部縱座標大於某個值後,回收該表項以上的所有表項 if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here //回收索引為0到i-1的表項 recycleChildren(recycler, 0, i); return; } } } } ... }
recycleViewsFromStart()
通過遍歷找到滑出螢幕的表項,然後呼叫了recycleChildren()
回收他們:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { /** * Recycles children between given indices. * 回收孩子 * * @param startIndex inclusive * @param endIndexexclusive */ private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { if (startIndex == endIndex) { return; } if (DEBUG) { Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); } if (endIndex > startIndex) { for (int i = endIndex - 1; i >= startIndex; i--) { removeAndRecycleViewAt(i, recycler); } } else { for (int i = startIndex; i > endIndex; i--) { removeAndRecycleViewAt(i, recycler); } } } }
最終呼叫了父類LayoutManager.removeAndRecycleViewAt()
:
public abstract static class LayoutManager { /** * Remove a child view and recycle it using the given Recycler. * * @param index Index of child to remove and recycle * @param recycler Recycler to use to recycle child */ public void removeAndRecycleViewAt(int index, Recycler recycler) { final View view = getChildAt(index); removeViewAt(index); recycler.recycleView(view); } }
先從LayoutManager
中刪除表項,然後呼叫Recycler.recycleView()
回收表項:
public final class Recycler { /** * Recycle a detached view. The specified view will be added to a pool of views * for later rebinding and reuse. * * <p>A view must be fully detached (removed from parent) before it may be recycled. If the * View is scrapped, it will be removed from scrap list.</p> * * @param view Removed view for recycling * @see LayoutManager#removeAndRecycleView(View, Recycler) */ public void recycleView(View view) { // This public recycle method tries to make view recycle-able since layout manager // intended to recycle this view (e.g. even if it is in scrap or change cache) ViewHolder holder = getChildViewHolderInt(view); if (holder.isTmpDetached()) { removeDetachedView(view, false); } if (holder.isScrap()) { holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); } }
通過表項檢視拿到了對應ViewHolder
,然後把其傳入Recycler.recycleViewHolderInternal()
,現在就可以更準地回答上一篇的那個問題“回收些啥?”:
回收的是滑出螢幕表項對應的ViewHolder
。
public final class Recycler { ... int mViewCacheMax = DEFAULT_CACHE_SIZE; static final int DEFAULT_CACHE_SIZE = 2; final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); ... /** * internal implementation checks if view is scrapped or attached and throws an exception * if so. * Public version un-scraps before calling recycle. */ void recycleViewHolderInternal(ViewHolder holder) { ... if (forceRecycle || holder.isRecyclable()) { //先存在mCachedViews裡面 //這裡的判斷條件決定了複用mViewCacheMax中的ViewHolder時不需要重新繫結資料 if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view //如果mCachedViews大小超限了,則刪掉最老的被快取的ViewHolder int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { // when adding the view, skip past most recently prefetched views int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = mCachedViews.get(cacheIndex).mPosition; if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; } //ViewHolder加到快取中 mCachedViews.add(targetCacheIndex, holder); cached = true; } //若ViewHolder沒有入快取則存入回收池 if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { ... } ... }
-
通過
cached
這個布林值,實現互斥,即ViewHolder
要麼存入mCachedViews
,要麼存入pool
-
mCachedViews
有大小限制,預設只能存2個ViewHolder
,當第三個ViewHolder
存入時會把第一個移除掉,程式碼如下:
public final class Recycler { ... void recycleCachedViewAt(int cachedViewIndex) { if (DEBUG) { Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); } ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); if (DEBUG) { Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder); } //將ViewHolder加入到回收池 addViewHolderToRecycledViewPool(viewHolder, true); //將ViewHolder從cache中移除 mCachedViews.remove(cachedViewIndex); } ... }
從mCachedViews
移除掉的ViewHolder
會加入到回收池中。
mCachedViews
有點像“回收池預備佇列”,即總是先回收到mCachedViews
,當它放不下的時候,按照先進先出原則將最先進入的ViewHolder
存入回收池
:
public final class Recycler { /** * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool. * 將viewHolder存入回收池 * * Pass false to dispatchRecycled for views that have not been bound. * * @param holder Holder to be added to the pool. * @param dispatchRecycled True to dispatch View recycled callbacks. */ void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) { clearNestedRecyclerViewIfNotNested(holder); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) { holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE); ViewCompat.setAccessibilityDelegate(holder.itemView, null); } if (dispatchRecycled) { dispatchViewRecycled(holder); } holder.mOwnerRecyclerView = null; getRecycledViewPool().putRecycledView(holder); } } public static class RecycledViewPool { static class ScrapData { ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); //每種型別的ViewHolder最多存5個 int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } //以viewType為鍵,ScrapData為值,作為回收池中ViewHolder的容器 SparseArray<ScrapData> mScrap = new SparseArray<>(); //ViewHolder入池 按viewType分類入池,相同的ViewType存放在List中 public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; //如果超限了,則放棄入池 if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } //入回收池之前重置ViewHolder scrap.resetInternal(); scrapHeap.add(scrap); } }
ViewHolder
會按viewType
分類存入回收池,最終儲存在ScrapData
的ArrayList
中,回收池資料結構分析詳見RecyclerView快取機制(咋複用?)
。
快取優先順序
還記得RecyclerView快取機制(咋複用?) 中得出的結論嗎?這裡再引用一下:
-
雖然為了獲取
ViewHolder
做了5次嘗試(共從6個地方獲取),先排除3種特殊情況,即從mChangedScrap
獲取、通過id獲取、從自定義快取獲取,正常流程中只剩下3種獲取方式,優先順序從高到低依次是:
mAttachedScrap mCachedViews mRecyclerPool
- 這樣的快取優先順序是不是意味著,對應的複用效能也是從高到低?(複用效能越好意味著所做的昂貴操作越少)
ViewHodler ViewHolder ViewHolder
當時分析了mAttachedScrap
和mRecyclerPool
的複用效能,即
從mRecyclerPool
中複用的ViewHolder
需要重新繫結資料,從mAttachedScrap
中複用的ViewHolder
不要重新出建立也不需要重新繫結資料
把存入mCachedViews
的程式碼和複用時繫結資料的程式碼結合起來看一下:
void recycleViewHolderInternal(ViewHolder holder) { ... //滿足這個條件才能存入mCachedViews if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { } ... } ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) { ... //滿足這個條件就需要重新繫結資料 if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){ } ...
重新繫結資料的三個條件中,holder.needsUpdate()
和holder.isInvalid()
都是false
時才能存入mCachedViews
,而!holder.isBound()
對於mCachedViews
中的ViewHolder
來說必然為false
,因為只有當呼叫ViewHolder.resetInternal()
重置ViewHolder
後,才會將其設定為未繫結狀態,而只有存入回收池時才會重置ViewHolder
。所以
從mCachedViews
中複用的ViewHolder
不需要重新繫結資料
總結
-
滑出螢幕表項對應的
ViewHolder
會被回收到mCachedViews
+mRecyclerPool
結構中,mCachedViews
是ArrayList
,預設儲存最多2個ViewHolder
,當它放不下的時候,會通過將舊的ViewHolder
挪到mRecyclerPool
的方式來騰出空間。mRecyclerPool
是SparseArray
,它會按viewType
分類儲存ViewHolder
,預設每種型別最多存5個。 -
從
mRecyclerPool
中複用的ViewHolder
需要重新繫結資料 -
從
mCachedViews
中複用的ViewHolder
不需要重新繫結資料