RecyclerView 缓存复用机制简析

前言

谈到 RecyclerView,大家肯定都不陌生,基本上每个 Android APP 都会用到,它是 Android 中一个非常重要的组件,用于灵活、高效的展示大量数据集合。本文将通过 RecyclerView 的源码简单分析它的缓存复用机制。

下面开始。

1、RecyclerView 缓存和复用的基本知识

在了解缓存复用机制之前,先了解 RecyclerView 中的一个内部类:Recycler,这个类是 RecycelerView 缓存复用机制的核心。下面我们看看 Recycler 中的关于缓存复用机制的成员变量:

Java 复制代码
public final class Recycler {
        final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<RecyclerView.ViewHolder> mChangedScrap = null;

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

        RecyclerView.RecycledViewPool mRecyclerPool;

        private RecyclerView.ViewCacheExtension mViewCacheExtension;
    }

从上述代码可以看出,缓存复用的对象是 ViewHolder。上面五个成员变量就是我们常说的 RecyclerView 的四级缓存,分别是:

  1. mChangedScrap 与 mAttachedScrap,用来缓存还在屏幕内的 ViewHolder。
  2. mCachedViews,用来缓存移出屏幕之外的 ViewHolder。
  3. mViewCacheExtension,这一层的创建和缓存完全由开发者自己控制,初始值为 null。
  4. mRecyclerPool,ViewHolder 缓存池。

2、RecyclerView 复用流程分析

RecyclerView 的复用流程执行在 Recycler 的 tryGetViewHolderForPositionByDeadline() 方法中,来看源码中怎么执行复用操作:

Java 复制代码
tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        ......
        ......

        if (mState.isPreLayout()) {
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            ......
            ......
        }
        if (holder == null) {
            ......
            ......
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                        type, dryRun);
                ......
                ......
            }
            if (holder == null && mViewCacheExtension != null) {
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    ......
                    ......
                }
            }
            if (holder == null) { // fallback to pool
                ......
                holder = getRecycledViewPool().getRecycledView(type);
                ......
                ......
            }
            if (holder == null) {
                ......
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                ......
                ......
            }
        }

        ......
        ......
        
        boolean bound = false;
        if (mState.isPreLayout() && holder.isBound()) {
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                ......
                ......
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }    
        
        ......
        ......
        
        return holder;
    }

从上面的流程我们可以看到,这些 if 语句都会去判断 holder 是否已经从缓存中拿到并复用了,如果已经拿到,则后面的 if 语句都进不去,则运行到后续并返回 holder,否则 holder == null,则进行下一层的从缓存中复用。

由上到下按层级依次是:

  1. getChangedScrapViewForPosition() 方法,根据 position 和 stableId 从 mChangedScrap 中查找。
  2. getScrapOrHiddenOrCachedHolderForPosition() 方法,根据 position 从 mAttachedScrap 和 mCachedViews 中查找。
  3. getScrapOrCachedViewForId() 方法,根据 stableId 从 mAttachedScrap 和 mCachedViews 中查找。
  4. mViewCacheExtension.getViewForPositionAndType() 方法,根据自定义实现的缓存和复用规则运行,没有自定义则不会进入这步。
  5. getRecycledViewPool().getRecycledView() 方法,从缓存池中查找。

下面是 tryGetViewHolderForPositionByDeadline() 方法中的后续步骤:

如果上述的所有步骤都没有复用到 ViewHolder,则调用 mAdapter.createViewHolder() 方法直接新建一个 ViewHolder,此时调用到的就是 adapter 中的 onCreateViewHolder() 方法。

上述所有过程运行结束之后,此时不论是通过复用还是新建,holder 都已经不为 null 了,则会调用 tryBindViewHolderByDeadline() 方法进行数据处理,adapter 的 onBindViewHolder() 方法就是在这里调用到的。

上述各个方法具体的细节就不展开分析,有兴趣或需求的可以自己到源码里看看。

3、RecyclerView 缓存原理分析

上面提到了复用分了四层,而复用的 ViewHolder 对象就是从各层缓存中查找的,下面我们来分析缓存的机制。

mChangedScrap 与 mAttachedScrap 缓存

这层的缓存在 Recycler 的 scrapView() 方法中执行:

Java 复制代码
void scrapView(View view) {
        final RecyclerView.ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_REMOVED | RecyclerView.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"
                        + " recycler pool." + exceptionLabel());
            }
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<RecyclerView.ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }

这个方法主要用于缓存出现在屏幕内的 item,当我们调用了 adapter 的 notifyXXX() 方法通知 item 发生变化后,会使用 mAttachedScrap 缓存没有发生变化的 ViewHolder,其他的则由 mChangedScrap 缓存,添加 itemView 的时候快速从里面取出,完成局部刷新。

mCachedViews 与 mRecyclerPool 缓存

这两层缓存主要是在 Recycler 的 recycleView() 方法中:

Java 复制代码
public void recycleView(@NonNull View view) {
        ViewHolder holder = getChildViewHolderInt(view);
        if (holder.isTmpDetached()) {
            removeDetachedView(view, false);
        }
        if (holder.isScrap()) {
            holder.unScrap();
        } else if (holder.wasReturnedFromScrap()) {
            holder.clearReturnedFromScrapFlag();
        }
        recycleViewHolderInternal(holder);
        if (mItemAnimator != null && !holder.isRecyclable()) {
            mItemAnimator.endAnimation(holder);
        }
    }

其中的 recycleViewHolderInternal() 方法是关键,里面处理了 mCachedViews 和 mRecyclerPool 的缓存,继续点进去看源码:

Java 复制代码
void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
        ......
        ......
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_INVALID
                    | RecyclerView.ViewHolder.FLAG_REMOVED
                    | RecyclerView.ViewHolder.FLAG_UPDATE
                    | RecyclerView.ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }

                ......
                ......

                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        } else {
            ......
            ......
        }
        ......
        ......
    }

进入第一个 if 之后,会先判断 mViewCacheMax 是否大于 0,这个 mViewCacheMax 就是 mCachedViews 能保存的最大容量,默认是 2,因此这个条件满足,&& 后面的条件主要是判断当前 ViewHolder 的状态是否 不 处于"失效、移除、更新、未知"这四个状态,即已经移出屏幕,如果不满足这个条件,则直接调用 addViewHolderToRecycledViewPool() 方法将 ViewHolder 存入缓存池,否则,会继续判断当前 mCachedViews 是否已满,下面分两种情况:

  • 已满
    调用 recycleCachedViewAt() 方法:
Java 复制代码
void recycleCachedViewAt(int cachedViewIndex) {
        if (DEBUG) {
            Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
        }
        RecyclerView.ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
        if (DEBUG) {
            Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
        }
        addViewHolderToRecycledViewPool(viewHolder, true);
        mCachedViews.remove(cachedViewIndex);
    }

这里执行的流程是: 取出 mCachedViews 中第一个 ViewHolder,再调用上面提到过的 addViewHolderToRecycledViewPool() 方法将 ViewHolder 存入缓存池,最后从 mCachedViews 中移除掉这个 ViewHolder。

执行完上述流程后,mCachedViews 便有一个空闲位置,之后调用 mCachedViews.add() 方法顺利将需要回收的 ViewHolder 保存到 mCachedViews 中。

  • 未满
    未满则跳过了 if,直接调用 mCachedViews.add() 方法将需要回收的 ViewHolder 保存到 mCachedViews 中。

因此我们知道了:mCachedViews 中保存的 ViewHolder 一定是最近移出屏幕的,稍早移出屏幕的 ViewHolder 先被保存到 mCachedViews,随后被转存到缓存池中。

另外,还有一层 mViewCacheExtension 缓存,由于作者水平有限,开发过程中还没用到过,这里就不多说。

最后补充一个 RecyclerView 使用过程中的操作:

如果在 RecyclerView 滑动过程中出现 View 的闪烁或者数据绑定错乱,可以调用 adapter 的 setHasStableIds(),并传入参数 true,此方法告诉 RecyclerView 的 Adapter 每个 item 的 id 是否固定不变的,hasStableIds 为 true 时,意味着相同的 id 总是指向相同的数据项,每个数据项的 id 在 RecyclerView 的生命周期内应该是唯一且固定的,RecyclerView 可以使用这些 id 来确定哪些项目保持不变,从而实现更有效的视图缓存和复用策略,特别是在数据集发生变化时。这有助于减少不必要的视图创建和绑定,显著提升滑动和更新时的性能。

👉 以上就是对 RecyclerView 缓存复用机制的简单分析。

相关推荐
overmind4 分钟前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时9 分钟前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
恋猫de小郭12 分钟前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
庄园特聘拆椅狂魔12 分钟前
从连连看到粒子消散——前端小游戏与视觉特效的技术选型
前端
JudithHuang29 分钟前
React 常用 Hooks
前端·react.js·前端框架
sycmancia1 小时前
Qml——Window元素使用
前端
每天都好困2 小时前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端
每天都好困2 小时前
04 地图图层封装:像管理 Photoshop 图层一样管理地图元素
前端
去伪存真2 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent
niucloud-admin2 小时前
JAVA V6 多商户商城 开发文档——手机端前端
java·开发语言·前端