RecyclerView緩存機制(回收去哪?)

這是RecyclerView緩存機制系列文章的第三篇,系列文章的目錄以下:android

  1. RecyclerView緩存機制(咋複用?)
  2. RecyclerView緩存機制(回收些啥?)
  3. RecyclerView緩存機制(回收去哪?)
  4. RecyclerView緩存機制(scrap view)

若是想直接看結論能夠移步到第四篇末尾(你會後悔的,過程更加精彩)。緩存

回收入口

上一篇以列表滑動事件爲起點沿着調用鏈一直往下尋找,驗證了「滑出屏幕的表項」會被回收。那它們被回收去哪裏了?沿着上一篇的調用鏈繼續往下探究:bash

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 dt       This 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 endIndex   exclusive
     */
    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()app

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()回收表項:ide

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(),如今就能夠更準地回答上一篇的那個問題「回收些啥?」:回收的是滑出屏幕表項對應的ViewHolderpost

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分類存入回收池,最終存儲在ScrapDataArrayList中,回收池數據結構分析詳見RecyclerView緩存機制(咋複用?)fetch

緩存優先級

還記得RecyclerView緩存機制(咋複用?)中得出的結論嗎?這裏再引用一下:ui

  • 雖然爲了獲取ViewHolder作了5次嘗試(共從6個地方獲取),先排除3種特殊狀況,即從mChangedScrap獲取、經過id獲取、從自定義緩存獲取,正常流程中只剩下3種獲取方式,優先級從高到低依次是:
  1. mAttachedScrap獲取
  2. mCachedViews獲取
  3. mRecyclerPool獲取
  • 這樣的緩存優先級是否是意味着,對應的複用性能也是從高到低?(複用性能越好意味着所作的昂貴操做越少)
  1. 最壞狀況:從新建立ViewHodler並從新綁定數據
  2. 次好狀況:複用ViewHolder但從新綁定數據
  3. 最好狀況:複用ViewHolder且不從新綁定數據

當時分析了mAttachedScrapmRecyclerPool的複用性能,即 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結構中,mCachedViewsArrayList,默認存儲最多2個ViewHolder,當它放不下的時候,按照先進先出原則將最早進入的ViewHolder存入回收池的方式來騰出空間。mRecyclerPoolSparseArray,它會按viewType分類存儲ViewHolder,默認每種類型最多存5個。
  • mRecyclerPool中複用的ViewHolder須要從新綁定數據
  • mCachedViews中複用的ViewHolder不須要從新綁定數據
相關文章
相關標籤/搜索