滚动时RecyclerView对ViewHolder的缓存与复用

滚动时ViewHolder的复用

首先需要对RecyclerView如何滚动显示其子项有一定的了解,这与ViewGroup的显示流程有关,ViewGroup需要依次走过onMeasure()方法、onLayout()方法、onDraw()方法,依次测量,布局,绘制。RecyclerView监听到滑动的操作后,重新调用onLayout()来排列和布局子 View ,这样就有了滚动的能力。

如果稍微跟踪一下onLayout的调用链,会是这样的

RecyclerView.onLayout() 
    --> RecyclerView.dispatchLayout()
    --> dispatchLayoutStep1(), dispatchLayoutStep2(), dispatchLayoutStep3();
void dispatchLayout() {
        ......
        mState.mIsMeasuring = false;
        // 没有进行过布局流程
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        // 执行过布局,但是因为数据变化或布局大小变化需要重新布局
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // 设置RecyclerView的宽高为精确模式(即MeasureSpecMode == MeasureSpec.EXACTLY)
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

设置了Adapter和LayoutManager的话,dispatchLayoutStep3()是一定会被调用的。

但是其实dispatchLayoutStep1()dispatchLayoutStep3()主要和动画有关,dispatchLayoutStep2()才是缓存复用机制的关键。

private void dispatchLayoutStep2() {
        ......

    // Step 2: Run layout
    mState.mInPreLayout = false;
    // 这里
    mLayout.onLayoutChildren(mRecycler, mState);
    
    mState.mStructureChanged = false;
    mPendingSavedState = null;

       ......
}

变量mLayout类型是LayoutManager,也是初始化RecyclerView的时候需要设置的布局管理器,可以选择LinearLayoutManager或者GridLayoutManager。

mRecycler的类型为Recycler,它是 RecyclerView 的内部类。

public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();

    RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;
}

Recycler 的主要成员变量为用来缓存和复用 ViewHolder 的集合,可以说是这套复用机制的核心。


知识补充:

这些缓存集合可以分为 4 个级别,按优先级从高到低为:

  • 一级缓存:mAttachedScrap 和 mChangedScrap ,为LayoutManager每次布局子View之前,那些已经添加到RecyclerView中的Item以及被删除的Item的临时存放集合(下方将会分析到)

    • mAttachedScrap 存储的是当前还在屏幕中的 ViewHolder。

    • mChangedScrap 存储的是当前还在屏幕中、数据已经改变的 ViewHolder 。

  • 二级缓存:mCachedViews ,用来缓存移除屏幕之外的 ViewHolder。

    • 默认情况下缓存容量是 2,可以通过 setViewCacheSize 方法来改变缓存的容量大小
    • 如果 mCachedViews 的容量已满,则会根据 FIFO 的规则移除旧 ViewHolder。
    • 取出ViewHolder(即将重用)时无须重新绑定数据(不用执行onBindViewHolder方法)
  • 三级缓存:ViewCacheExtension ,开发给用户的自定义扩展缓存,需要用户自己管理 View 的创建和缓存。

    相当不常用。

  • 四级缓存:RecycledViewPool ,ViewHolder 缓存池,在有限的 mCachedViews 中如果存不下新的 ViewHolder 时,就会把 ViewHolder 存入RecyclerViewPool 中。

    • 按照 Type 来查找 ViewHolder,与之相比,上面的都是通过index去获取 ViewHolder的

    • 每个 Type 默认最多缓存 5 个

    • 可以多个 RecyclerView 共享 RecycledViewPool


我们以LinearLayoutManager为例,继续分析调用

调用链如下:

mLayout.onLayoutChildren(mRecycler, mState) 
--> LinearLayoutManager.onLayoutChildren(...) 
--> LinearLayoutManager.fill(...) 
--> LinearLayoutManager.layoutChunk(recycler, layoutState)
--> LinearLayoutManager.LayoutState.next(recycler) 

LinearLayoutManager.onLayoutChildren(...)那一段很长的源码有点难理解,总结起来就是两步

  1. 通过检查childView和其他变量,找出锚点的坐标(coordinate)和位置(position),并把锚点信息设置到AnchorInfo
  2. 根据锚点向两边填充。(填充两边的先后顺序根据条件不同而不同)
    public void onLayoutChildren(
        RecyclerView.Recycler recycler, RecyclerView.State state) {
        
        ......
        // 这里会将所有子view detach并通过scrap回收(一级缓存)
        detachAndScrapAttachedViews(recycler);
        ......
        if (mAnchorInfo.mLayoutFromEnd) {
            ......
        // 下面的代码是从顶端到底端开始布局,上面的则恰好相反
        } else {
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            // 第一次fill
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            // 第二次fill
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            // 如果屏幕上还有剩余空间
            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtraFillSpace = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }
    }

根据步骤,我们找到了调用链的下一层LinearLayoutManager.fill(...)这个填充itemView的方法,因为需要向两边填充,这个方法在填充过程中会被调用两次。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        ......
         // while循环调用,每次调用填充一个 ItemView 到 RecyclerView,填充由layoutChunk()完成
        while ((layoutState.mInfinite || remainingSpace > 0) 
               && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            // 这里调用了layoutChunk
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            
            ......
            // 如果是因滚动引起的布局,会通过判断滑动后view是否滑出边界决定是否回收View
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                // 这里的回收操作最终回调Recycler.recycleViewHolderInternal()
                recycleByLayoutState(recycler, layoutState);
            }
            ......
    }

recycleByLayoutState方法承担对没有再次布局的Item进行缓存(回收)的任务。回调Recycler.recycleViewHolderInternal()便意味着被回收的Item回到了二级以及更低的缓存中。

layoutChuck()倒没有遮遮掩掩,一开始就给出了填充View的方法

View view = layoutState.next(recycler);
View next(RecyclerView.Recycler recycler) {
    // 除了我们平常认知的RecyclerView中Recycler的缓存
    // 还有一个缓存mScrapList,被LayoutManager持有
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    // 这里调用了Recycler来获取View
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

在Recycler下的调用链

Recycler.getViewForPosition(int) 
--> Recycler.getViewForPosition(int, boolean) 
--> Recycler.tryGetViewHolderForPositionByDeadline(...)

最终的tryGetViewHolderForPositionByDeadline()中便如同预料一般,先从一级缓存取ViewHolder,取不到则去二级缓存…直到需要创建一个新的ViewHolder。

@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    ......
    if (mState.isPreLayout()) {
        // 0) 预布局从 mChangedScrap 里面去获取 ViewHolder,动画相关
        holder = getChangedScrapViewForPosition(position);
    }

    if (holder == null) {
        // 1) 分别从 mAttachedScrap、 mHiddenViews、mCachedViews 获取 ViewHolder
        //    这个 mHiddenViews 是用来做动画期间的复用
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }

    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 如果 Adapter 的 hasStableIds 方法返回为 true
        //    优先通过 ViewType 和 ItemId 两个条件从 mAttachedScrap 和 mCachedViews 寻找
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                               type, dryRun);
        }

        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because 
            // LayoutManager does not know it.
            // 3) 从自定义缓存获取,别问,问就是别用
            View view = mViewCacheExtension
                getViewForPositionAndType(this, position, type);
            holder = getChildViewHolder(view);
        }
    }

    if (holder == null) {
        // 4) 从 RecycledViewPool 获取 ViewHolder
        holder = getRecycledViewPool().getRecycledView(type);
    }

    if (holder == null) {
        // 缓存全取过了,没有,那只好 create 一个咯
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
    boolean bound = false;
    // 只要满足以下3个情况:
    //1、ViewHolder没有被绑定过,即没有设置FLAG_BOUND标志位
    //2、ViewHolder需要更新,即设置了FLAG_UPDATE标志位
    //3、ViewHolder是无效的,即设置了FLAG_INVALID标志位
    //就会调用Adapter中的OnBindViewHolder方法
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException(
                "Removed holder should be bound and it should"
                + " come here only in pre-layout. Holder: " + holder
                + exceptionLabel());
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        // 这里最终调用Adapter中的OnBindViewHolder方法
        bound = tryBindViewHolderByDeadline(holder, offsetPosition,
                                            position, deadlineNs);
    }
    ......
}
           

复用分析结束。

ViewHolder的缓存

先来看看Recycler中对于缓存的方法

  1. recycleViewHolderInternal(ViewHolder holder)

    上面提到过这个方法会把ViewHolder回收到mCacheViews和RecyclerViewPool

     void recycleViewHolderInternal(ViewHolder holder) {
                ......
                if (forceRecycle || holder.isRecyclable()) {
                    if (mViewCacheMax > 0
                            && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_REMOVED
                            | ViewHolder.FLAG_UPDATE
                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                        // Retire oldest cached view
                        int cachedViewSize = mCachedViews.size();
                        if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                             // mCacheViews 满了,最早加入的不要了放 RecyclerViewPool
                            recycleCachedViewAt(0);
                            cachedViewSize--;
                        }
                        ......
                        mCachedViews.add(targetCacheIndex, holder);
                        cached = true;
                    }
                    if (!cached) {
                        // 不能放进 mCacheViews 的放 RecyclerViewPool
                        addViewHolderToRecycledViewPool(holder, true);
                        recycled = true;
                    }
                } else {
                   ......
                }
            }
    
  2. recycleCachedViewAt(int cachedViewIndex)

    这个方法会将ViewHolder从mCacheViews(移除后)回收到RecyclerViewPool

    至于addViewHolderToRecycledViewPool则望文生义,直接添加进入RecyclerViewPool

    // Recycles a cached view and removes the view from the list
       void recycleCachedViewAt(int cachedViewIndex) {
         ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
         addViewHolderToRecycledViewPool(viewHolder, true);
         mCachedViews.remove(cachedViewIndex);
       }
    
  3. scrapView(View view)

    这个方法把ViewHolder存放在一级缓存的Scrap内

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
         // 标记为移除或失效的 || 完全没有改变 || item 无动画或动画不复用,加入到AttachedScrap
        if (holder.hasAnyOfTheFlags(
            ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            if (holder.isInvalid() && !holder.isRemoved()
                && !mAdapter.hasStableIds()) {
                throw new IllegalArgumentException(
                    "Called scrap view with an invalid view."
                    + " Invalid views cannot be reused from scrap,
                    + " they should rebound from" + ceptionLabel());
            }
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);
        } else {
            // 否则加入到ChangedScrap
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }
    

虽然第一节内容是沿着滚动的思考追溯复用,但其实我们也遇见了缓存的方法。

public void onLayoutChildren() --> detachAndScrapAttachedViews(recycler);
int fill(...) -->  recycleByLayoutState(recycler, layoutState);

跟踪一下detachAndScrapAttachedViews(recycler)

       public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                // 这里
                scrapOrRecycleView(recycler, i, v);
            }
        }

        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            // ViewHolder如果完全被LayoutManager所管理,则忽略,不对其回收
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            // 如果ViewHolder失效,并且未被移除并且适配器的项具有稳定的id(id有效)
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                // 回收到二级及更低的缓存
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                // 回收到一级缓存
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

recycleByLayoutState(recycler, layoutState)的调用链如下

    LinearLayoutManager.recycleByLayoutState(recycler, layoutState)
--> recycleViewsFromStart(...)recycleViewsFromEnd(...)
--> recycleChildren(...)
--> removeAndRecycleViewAt(...)
--> Recycler.recycleView(view)
--> Recycler.recycleViewHolderInternal(holder)

参考资料

RecycylerView之布局设计

每日一问 | RecyclerView的多级缓存机制,每级缓存到底起到什么样的作用?

RecyclerView 的缓存复用机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值