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. mChangedScrapmAttachedScrap,用来缓存还在屏幕内的 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 从 mAttachedScrapmCachedViews 中查找。
  3. getScrapOrCachedViewForId() 方法,根据 stableId 从 mAttachedScrapmCachedViews 中查找。
  4. mViewCacheExtension.getViewForPositionAndType() 方法,根据自定义实现的缓存和复用规则运行,没有自定义则不会进入这步。
  5. getRecycledViewPool().getRecycledView() 方法,从缓存池中查找。

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

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

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

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

3、RecyclerView 缓存原理分析

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

mChangedScrapmAttachedScrap 缓存

这层的缓存在 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 的时候快速从里面取出,完成局部刷新。

mCachedViewsmRecyclerPool 缓存

这两层缓存主要是在 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() 方法是关键,里面处理了 mCachedViewsmRecyclerPool 的缓存,继续点进去看源码:

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 缓存复用机制的简单分析。

相关推荐
耶啵奶膘1 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^3 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic4 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具4 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161775 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test6 小时前
js下载excel示例demo
前端·javascript·excel
Yaml46 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事6 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro