RecyclerView缓存机制(回收去哪?)

作者: 唐子玄 | 来源:发表于2019-02-17 12:20 被阅读23次

    这是RecyclerView缓存机制系列文章的第三篇,系列文章的目录如下:

    1. RecyclerView缓存机制(咋复用?)
    2. RecyclerView缓存机制(回收些啥?)
    3. 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 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()

    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分类存入回收池,最终存储在ScrapDataArrayList中,回收池数据结构分析详见RecyclerView缓存机制(咋复用?)

    缓存优先级


    还记得RecyclerView缓存机制(咋复用?)中得出的结论吗?这里再引用一下:

    • 虽然为了获取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挪到mRecyclerPool的方式来腾出空间。mRecyclerPoolSparseArray,它会按viewType分类存储ViewHolder,默认每种类型最多存5个。
    • mRecyclerPool中复用的ViewHolder需要重新绑定数据
    • mCachedViews中复用的ViewHolder不需要重新绑定数据

    相关文章

      网友评论

        本文标题:RecyclerView缓存机制(回收去哪?)

        本文链接:https://www.haomeiwen.com/subject/eezweqtx.html