RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?

简介: 缓存是 RecyclerView 时间性能优越的重要原因。缓存池是所有缓存中速度最慢的。这一篇从源码出发,探究哪些情况下表项会被缓存到缓存池。

缓存是 RecyclerView 时间性能优越的重要原因。缓存池是所有缓存中速度最慢的,其中的ViewHodler是脏的,得重新执行onBindViewHolder()。这一篇从源码出发,探究哪些情况下“表项会被回收到缓存池”。

缓存池结构

在分析不同的回收场景前,先回顾一下“缓存池是什么?”

表项被回收到缓存池,在源码上的表项为 ViewHolder 实例被存储到RecycledViewPool结构中:

public class RecyclerView {
    public final class Recycler {
        // 回收表项视图
        public void recycleView(@NonNull View view) {
            ViewHolder holder = getChildViewHolderInt(view);
            // 回收表项 ViewHolder
            recycleViewHolderInternal(holder);
        }
        // 回收 ViewHolder
        void recycleViewHolderInternal(ViewHolder holder) {
            ...
            // 将 ViewHolder 存入缓存池
            addViewHolderToRecycledViewPool(holder, true);
        }

        // 将 ViewHolder 实例存储到 RecycledViewPool 结构中
        void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            ...
            getRecycledViewPool().putRecycledView(holder);
        }
        // 获取 RecycledViewPool 实例
        RecycledViewPool getRecycledViewPool() {
            if (mRecyclerPool == null) {
                mRecyclerPool = new RecycledViewPool();
            }
            return mRecyclerPool;
        }
    }
    // 缓存池
    public static class RecycledViewPool {
        // 单类型缓存列表
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        }
        // 多类型缓存列表构成的缓存池(以 int 为键)
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        public void putRecycledView(ViewHolder scrap) {
            // 获取 ViewHolder 类型
            final int viewType = scrap.getItemViewType();
            // 获取指定类型的 ViewHolder 缓存列表
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            ...
            // ViewHolder 实例存入缓存列表
            scrapHeap.add(scrap);
        }
    }
}

RecycledViewPool用一个SparseArray将不同类型的 ViewHolder 实例缓存在内存,每种类型对应一个列表。当有相同类型的表项插入列表时,不用重新创建 ViewHolder 实例(执行 onCreateViewHolder()),从缓存池中获取即可。

关于缓存池的详细解析可以点击RecyclerView 缓存机制 | 回收到哪去?

1. 表项主动移出屏幕

这种回收表项的场景是最常见的。效果图如下:

为啥要等 item 3 滚出屏幕后,item 1 才刚刚被回收,而 item 4 滚出屏幕后,item 2 立马被回收了?

这是因为mCachedViews的存在,它是默认大小为 2 的列表。用于缓存移出屏幕表项的 ViewHolder。

所有移出的表项都会依次被缓存至其中,当mCachedViews满时,按照先进先出原则,将最先存入的 ViewHolder 实例移除并转存至RecycledViewPool,即缓存池中。

所以 item 1 和 2 移出屏幕时,正好填满mCachedViews,当 item 3 移出屏幕时,item 1 就被挤出并存入缓存池。更详细的源码跟踪分析可以点击RecyclerView 缓存机制 | 回收到哪去?

那 RecyclerView 在滚动中是如何判断哪些表项应该被回收?

上一篇文章中详细分析了列表滚动时,表项是如何被回收的,现援引结论和图示如下。

  1. RecyclerView 在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。在填充表项的同时,也会回收表项,回收的依据是 limit 隐形线
  2. limit 隐形线 是 RecyclerView 在滚动发生之前根据滚动位移计算出来的一条线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠。
  3. limit 隐形线 的初始值 = 列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到 limit 值之上,即limit 隐形线会随着新表项的填充而不断地下移。
  4. 触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于limit 隐形线下方,则该表项上方的所有表项都会被回收。

下图形象地描述了 limit 隐形线(图中红色虚线):

回收逻辑落实在源码上,就是如下(0-5)的调用链:

public class RecyclerView {
    public final class Recycler {
        // 5
        public void recycleView(View view) {...}
    }
    
    public abstract static class LayoutManager {
        public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            // 4
            recycler.recycleView(view);
        }
    }
}

public class LinearLayoutManager {
    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
        // 3:回收索引值为 endIndex -1 到 startIndex 的表项
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }
    
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
        ...
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedEnd(child) > limit|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                // 2
                recycleChildren(recycler, 0, i);
            }
        }
    }
    
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        // 1
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
    
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 循环填充表项
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // 填充单个表项
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                // 0:回收表项
                recycleByLayoutState(recycler, layoutState);
            }
            ...
        }
    }
}

每填充一个表项都会遍历已加载的所有表项,以检测其中是否有可以回收的。

若对结论的源码分析过程感兴趣,可以点击RecyclerView 面试题 | 滚动时表项是如何被填充或回收的?

2. 表项被挤出屏幕

当列表中有表项插入,把现有表项挤出屏幕时,也会发生表项回收。效果图如下:

这种场景下 item 2 会被回收,当表项动画完成后,就会触发表项回收逻辑:

// RecyclerView 默认表项动画器
public class DefaultItemAnimator extends SimpleItemAnimator {
    // 启动表项位移动画
    void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        final ViewPropertyAnimator animation = view.animate();
        animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animator) {
                // 往上分发动画结束事件
                dispatchMoveFinished(holder);
                ...
            }
        }).start();
    }
}

public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
    public final void dispatchMoveFinished(RecyclerView.ViewHolder item) {
        // 继续往上分发动画结束事件
        dispatchAnimationFinished(item);
    }
}

public class RecyclerView {
    public abstract static class ItemAnimator {
        private ItemAnimatorListener mListener = null;
        public final void dispatchAnimationFinished(ViewHolder viewHolder) {
            // 将动画结束事件分发给监听器
            if (mListener != null) { mListener.onAnimationFinished(viewHolder); }
        }
    }
    
    private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {
        @Override
        public void onAnimationFinished(ViewHolder item) {
            // 设置 ViewHolder 为可回收的
            item.setIsRecyclable(true);
            // 回收表项
            if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
                removeDetachedView(item.itemView, false);
            }
        }
    }
    
    boolean removeAnimatingView(View view) {
        startInterceptRequestLayout();
        final boolean removed = mChildHelper.removeViewIfHidden(view);
        // 当表项做完位移动画后确实移出了屏幕
        if (removed) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            mRecycler.unscrapView(viewHolder);
            // 回收 ViewHolder
            mRecycler.recycleViewHolderInternal(viewHolder);
        }
        ...
        return removed;
    }
}

RecyclerView 的表项动画器将移动表项动画的结束事件层层传递,最终传递到了 RecyclerView 内部的监听器,由监听器通知 Recycler 触发表项回收动作。

3. 高速缓存命中的 ViewHolder 变脏

变脏的意思是表项需要重绘,即调用onBindViewHolder()重新为表项绑定数据。

RecyclerView 中有四级缓存,它会优先去高速缓存中找 ViewHolder 实例。缓存池是其中速度最慢的,因为从中取出的 ViewHolder 需要重新执行onBindViewHolder()scrapview cache的速度都比它快,但命中后需要进行额外的校验(关于四级缓存的详解可以点击这里):

public class RecyclerView
    public final class Recycler {
        // RecyclerView 获取 ViewHolder 的入口
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
            // 从 scrap 或 view cache 中获取 ViewHolder 实例
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                // 若缓存命中
                if (holder != null) {
                    // 校验 ViewHolder
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        // 校验失败
                        if (!dryRun) {// dryRun 始终为 false
                            ....
                            // 回收命中的 ViewHolder (丢到缓存池)
                            recycleViewHolderInternal(holder);
                        }
                        // 标记从 scrap 或 view cache 中获取缓存失败
                        // 会触发从其他缓存继续获取 ViewHolder实例
                        holder = null;
                    } else {
                        // 标记校验成功
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            ....
        }
    }
}

从 scrap 或 view cache 命中的 ViewHolder 会从三个方面被校验:

  1. 表项是否被移除
  2. 表项 viewType 是否相同
  3. 表项 id 是否相同
public class RecyclerView{
    public final class Recycler {
        // 校验 ViewHolder 合法性
        boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
            // 如果表项已被移除
            if (holder.isRemoved()) {
                // 是否在 preLayout 阶段
                return mState.isPreLayout();
            }

            if (!mState.isPreLayout()) {
                // 检查从缓存中获取的 ViewHolder 是否和 Adapter 对应位置的 ViewHolder 有相同的 viewType
                final int type = mAdapter.getItemViewType(holder.mPosition);
                if (type != holder.getItemViewType()) {
                    return false;
                }
            }
            // 检查从缓存中获取的 ViewHolder 是否和 Adapter 对应位置的 ViewHolder 有相同的 id
            if (mAdapter.hasStableIds()) {
                return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
            }
            return true;
        }
    }
}

只有和指定位置表项具有相同的 viewType 或相同的 id 时,scrapview cache中命中的缓存才会被使用。否则即使命中也会视为无效ViewHolder被丢到缓存池中。

4. mCachedViews 中缓存的表项被删除

表项移出屏幕后,立刻被回收到mCachedViews结构中。若恰巧该表项又被删除了,则表项对应的 ViewHolder 从mCachedViews结构中移除,并添加到缓存池中:

public class RecyclerView {
    public final class Recycler {
        void recycleCachedViewAt(int cachedViewIndex) {
            // 从 mCacheViews 结构中获取指定位置的 ViewHolder 实例
            ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
            // 将 ViewHolder 存入缓存池
            addViewHolderToRecycledViewPool(viewHolder, true);
            // 将 ViewHolder 从 mCacheViews 中移除
            mCachedViews.remove(cachedViewIndex);
        }
        
        void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            ...
            getRecycledViewPool().putRecycledView(holder);
        }
    }
}

5. pre-layout 中额外填充的表项在 post-layout 中被移除

pre-layout & post-layout

pre-layoutpost-layoutRecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系有介绍过,援引如下:

RecyclerView 要做表项动画,

为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”

为了获得两张快照,就得布局两次,分别是 pre-layout 和 post-layout(布局即是往列表中填充表项),

为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),

但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),

RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 缓存中,以便在填充表项可以命中缓存,以缩短填充表项耗时。

Gif 的场景中,在 pre-layout 阶段,item 1、item 2、item 3 被填充到列表中,形成一张动画前的表项快照。而 post-layout 将 item 1、item 3 填充到列表中,形成一张动画后的表项快照。

对比这两张快照中的 item 3 的位置就能知道它该从哪里平移到哪里,也知道 item 2 需要做消失动画,当动画结束后,item 2 的 ViewHolder 会被回收到缓存池,回收的调用链和“表项被挤出屏幕”是一样的,都是由动画结束来触发的。

在 pre-layout 阶段填充额外表项

考虑另外一种场景,这次不是移除 item 2,而是更新它,比如把 item 2 更新成 item 2.1,那 pre-layout 还会将 item 3 填充进列表吗?

RecyclerView 动画原理 | 换个姿势看源码(pre-layout) 详细分析了,在 pre-layout 阶段,额外的表项是如何被填充到列表,其中关键源码再拿出来看一下:

public class LinearLayoutManager{
    // 向列表中填充表项
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 计算剩余空间
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 循环填充表项,直到没有剩余空间
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            // 填充单个表项
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
            // 在列表剩余空间中扣除刚填充表项所消耗的空间
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                remainingSpace -= layoutChunkResult.mConsumed;
            }
            ...
        }
        ...
    }
}

直觉上,每填充一个表项都应该将其消耗的空间扣除,但扣除逻辑套在了一个 if 中,即扣除是有条件的。

条件表达式中一共有三个条件,在预布局阶段!state.isPreLayout()必然是 false,layoutState.mScrapList != null也是 false(断点告诉我的),最后一个条件!layoutChunkResult.mIgnoreConsumed起了决定性的作用,它在填充单个表项时被赋值:

public class LinearLayoutManager {
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // 获取下一个该被填充的表项视图
        View view = layoutState.next(recycler);
        ...// 省略了实施填充的具体逻辑
        // 如果表项被移除或被更新 则 mIgnoreConsumed 置为 true
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        ...
    }
}

layoutChunkResult被作为参数传入layoutChunk(),并且当填充表项是被删除的或是被更新的,就将layoutChunkResult.mIgnoreConsumed置为 true。表示该表项虽然被填充进了列表但是它占用的空间应该呗忽略。至此可以得出结论:

在预布局阶段,循环填充表项时,若遇到被移除的或是被更新的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。

虽然这结论就是代码的本意,但还是有一点让我不太明白。忽略被移除表项占用的空间容易理解,那为啥更新的表项也一同被忽略?

那是因为,更新表项时,表项的布局可能发生变化(取决于onBindViewHolder()的实现),万一表项布局变长,则会造成其他表项被挤出屏幕,或是表项变短,造成新表项移入屏幕。

记录表项动画信息

RecyclerView 动画原理 | 如何存储并应用动画属性值?中介绍了 RecyclerView 是如何存储动画属性值的,现援引如下:

  1. RecyclerView 将表项动画数据封装了两层,依次是ItemHolderInfoInfoRecord,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。
  2. InfoRecord被集中存放在一个商店类ViewInfoStore中。所有参与动画的表项的ViewHolderInfoRecord都会以键值对的形式存储其中。
  3. RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以InfoRecord中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator,以触发动画。

在 pre-layout 阶段,存储动画信息的代码如下:

public class RecyclerView {
    private void dispatchLayoutStep1() {
            ...
            // 遍历列表中现有表项
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                // 为表项构建 ItemHolderInfo 实例
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),holder.getUnmodifiedPayloads());
                // 将 ItemHolderInfo 实例存入 ViewInfoStore
                mViewInfoStore.addToPreLayout(holder, animationInfo);
            }
            ...
            // 预布局
            mLayout.onLayoutChildren(mRecycler, mState);
            // 预布局后,再次遍历所有孩子(预布局可能填充额外的表项)
            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                final View child = mChildHelper.getChildAt(i);
                final ViewHolder viewHolder = getChildViewHolderInt(child);
                // 过滤掉带有 FLAG_PRE 标志位的表项
                if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                    // 为额外填充的表项构建 ItemHolderInfo 实例
                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                    // 将 ItemHolderInfo 实例存入 ViewInfoStore
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
            ...
    }
}

class ViewInfoStore {
    void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.preInfo = info;
        // 添加 FLAG_PRE 标志位
        record.flags |= FLAG_PRE;
    }
    
    void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        // 添加 FLAG_APPEAR 标志位
        record.flags |= FLAG_APPEAR;
        record.preInfo = info;
    }
}

在 pre-layout 的前后,遍历了两次表项。

对于 Demo 的场景来说,第一次遍历,item 1 和 2 的动画属性被存入 ViewInfoStore 并添加了FLAG_PRE标志位。遍历结束后执行预布局,把屏幕之外的 item 3 也填充到列表中。再紧接着的第二次遍历中,item 3 的动画属性也会被存入 ViewInfoStore 并添加了FLAG_APPEAR标志位,表示该表项是在预布局过程中额外被填充的。

在 post-layout 阶段,为了形成动画后的表项快照,得清空列表,重新填充表项,出于时间性能的考虑,被移除表项的 ViewHolder 缓存到了 scrap 结构中(item 1 2 3的 ViewHodler 实例)。

重新向列表中填充 item 1 和更新后的 item 2,它们的 ViewHolder 实例可以从 scrap 结构中快速获取,不必再执行 onCreateViewHolder()。填充完后,列表的空间已经用完,而 scrap 结构中还剩一个 item 3 的 ViewHolder 实例。它会在 post-layout 阶段被添加新的标志位:

public class LinearLayoutManager {
    // 在 dispatchLayoutStep2() 中第二次调用 onLayoutChildren() 进行 post-layout
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        // 为动画而进行布局
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    }
    
    private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,RecyclerView.State state, int startOffset,int endOffset) {
        final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
        final int scrapSize = scrapList.size();
        // 遍历 scrap 结构
        for (int i = 0; i < scrapSize; i++) {
            RecyclerView.ViewHolder scrap = scrapList.get(i);
            final int position = scrap.getLayoutPosition();
            final int direction = position < firstChildPos != mShouldReverseLayout? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
            // 计算 scrap 结构中对应表项所占用的空间
            if (direction == LayoutState.LAYOUT_START) {
                scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
            } else {
                scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
            }
        }
        // mLayoutState.mScrapList 被赋值
        mLayoutState.mScrapList = scrapList;
        // 再次尝试填充表项
        if (scrapExtraStart > 0) {
            ...
            fill(recycler, mLayoutState, state, false);
        }

        if (scrapExtraEnd > 0) {
            ...
            fill(recycler, mLayoutState, state, false);
        }
        mLayoutState.mScrapList = null;
    }
    
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        // 分支1:把表项填充到列表中
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } 
        // 分支2:把表项动画信息存储到 ViewInfoStore 中
        else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
                // 委托给父类 LayoutManger
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        ...
    }
}

这次填充表项的layoutChunk()因为layoutState.mScrapList不为空,会走不一样的分支,即调用addDisappearingView()

public class RecyclerView {
    public abstract static class LayoutManager {
        public void addDisappearingView(View child) {
            addDisappearingView(child, -1);
        }
        
        public void addDisappearingView(View child, int index) {
            addViewInt(child, index, true);
        }
        
        private void addViewInt(View child, int index, boolean disappearing) {
            final ViewHolder holder = getChildViewHolderInt(child);
            if (disappearing || holder.isRemoved()) {
                // 置 FLAG_DISAPPEARED 标志位
                mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
            } else {
                mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
            }
            ...
        }
    }
}

class ViewInfoStore {
    // 置 FLAG_DISAPPEARED 标志位
    void addToDisappearedInLayout(RecyclerView.ViewHolder holder) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.flags |= FLAG_DISAPPEARED;
    }
」

至此 item 3 在经历了 pre-layout 和 post-layout 后,它的动画信息被存储在ViewInfoStore中,且添加了两个标志位,分别是FLAG_APPEARFLAG_DISAPPEARED

在布局的第三阶段,会调用ViewInfoStore.process()触发动画:

public class RecyclerView {
    private void dispatchLayoutStep3() {
        ...
        // 触发表项执行动画
        mViewInfoStore.process(mViewInfoProcessCallback);
        ...
    }
}

class ViewInfoStore {
    void process(ProcessCallback callback) {
        // 遍历所有参与动画表项的位置信息
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            // 获取表项 ViewHolder
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            // 获取与 ViewHolder 对应的动画信息
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            // 根据动画信息的标志位确定动画类型以执行对应的 ProcessCallback 回调
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                callback.unused(viewHolder);
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                ...
            }
        }
    }
}

Demo 中的 item 3 会命中第一个 if 条件,因为:

class ViewInfoStore {
    static class InfoRecord {
        // 在 post-layout 中消失
        static final int FLAG_DISAPPEARED = 1;
        // 在 pre-layout 中出现
        static final int FLAG_APPEAR = 1 << 1;
        // 上两者的合体
        static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
    }
}

回收 item 3 到缓存池的逻辑就在callback.unused(viewHolder)中:

public class RecyclerView {
    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = new ViewInfoStore.ProcessCallback() {
                ...
                @Override
                public void unused(ViewHolder viewHolder) {
                    // 回收没有用的表项
                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
                }
            };
            
    public abstract static class LayoutManager {
        public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
            removeView(child);
            // 委托给 Recycler
            recycler.recycleView(child);
        }
    }
    
    public final class Recycler {
            public void recycleView(@NonNull View view) {
                // 回收表项到缓存池
                recycleViewHolderInternal()
        }
    }
}

至此可以得出结论:

所有在 pre-layout 阶段被额外填充的表项,若最终没能在 post-layout 阶段也填充到列表中,就都会被回到到缓存池。

推荐阅读

RecyclerView 系列文章目录如下:

  1. RecyclerView 缓存机制 | 如何复用表项?
  2. RecyclerView 缓存机制 | 回收些什么?
  3. RecyclerView 缓存机制 | 回收到哪去?
  4. RecyclerView缓存机制 | scrap view 的生命周期
  5. 读源码长知识 | 更好的RecyclerView点击监听器
  6. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
  7. 更好的 RecyclerView 表项子控件点击监听器
  8. 更高效地刷新 RecyclerView | DiffUtil二次封装
  9. 换一个思路,超简单的RecyclerView预加载
  10. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
  11. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
  12. RecyclerView 动画原理 | 如何存储并应用动画属性值?
  13. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
  14. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
  15. RecyclerView 性能优化 | 把加载表项耗时减半 (一)
  16. RecyclerView 性能优化 | 把加载表项耗时减半 (二)
  17. RecyclerView 性能优化 | 把加载表项耗时减半 (三)
  18. RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
  19. RecyclerView 的滚动时怎么实现的?(二)| Fling
目录
相关文章
|
12天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
24天前
|
SQL 缓存 关系型数据库
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴因未能系统梳理MySQL缓存机制而在美团面试中失利。为此,尼恩对MySQL的缓存机制进行了系统化梳理,包括一级缓存(InnoDB缓存)和二级缓存(查询缓存)。同时,他还将这些知识点整理进《尼恩Java面试宝典PDF》V175版本,帮助大家提升技术水平,顺利通过面试。更多技术资料请关注公号【技术自由圈】。
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
|
30天前
|
缓存 监控 算法
小米面试题:多级缓存一致性问题怎么解决
【10月更文挑战第23天】在现代分布式系统中,多级缓存架构因其能够显著提高系统性能和响应速度而被广泛应用。
40 3
|
1月前
|
存储 缓存 NoSQL
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
71 8
|
4月前
|
SQL 缓存 Java
【面试官】Mybatis缓存有什么问题吗?
面试官:你说下对MyBatis的理解?面试官:那SqlSession知道吧?面试官:Mybatis的缓存有哪几种?面试官:那Mybatis缓存有什么问题吗?面试官:Mybatis分页插件是怎么
【面试官】Mybatis缓存有什么问题吗?
|
4月前
|
canal 缓存 NoSQL
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;先删除缓存还是先修改数据库,双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
|
3月前
|
缓存 NoSQL Redis
一天五道Java面试题----第九天(简述MySQL中索引类型对数据库的性能的影响--------->缓存雪崩、缓存穿透、缓存击穿)
这篇文章是关于Java面试中可能会遇到的五个问题,包括MySQL索引类型及其对数据库性能的影响、Redis的RDB和AOF持久化机制、Redis的过期键删除策略、Redis的单线程模型为何高效,以及缓存雪崩、缓存穿透和缓存击穿的概念及其解决方案。
|
4月前
|
canal 消息中间件 缓存
面试题:如何解决缓存和数据库的一致性问题?
面试题:如何解决缓存和数据库的一致性问题?
86 1