RecyclerView:真的是四级缓存吗?

1,368 阅读6分钟

四级缓存吗?

很多文章在介绍 RecyclerView 缓存时都会提到 RecyclerView 的四级缓存,先来看这所说的是那四级呢?

  • 一级缓存:mAttachedScrapmChangedScrap
  • 二级缓存:mCachedViews
  • 三级缓存:ViewCacheExtension 自定义缓存
  • 四级缓存:RecycledViewPool 缓存池

那么 RecyclerView 缓存真的是由四级组成的吗?慢慢来看

上面所说的四级缓存都在 RecyclerView 的内部类 Recycler 中定义。它也是实现 RecyclerView 缓存逻辑的主要类。

/**
 * mCachedViews 中最大缓存数
 */
mViewCacheMax = DEFAULT_CACHE_SIZE

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;

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

/**
 * 自定义缓存实现
 */
private ViewCacheExtension mViewCacheExtension;

/**
 * 缓存池
 */
RecycledViewPool mRecyclerPool;

其中 mViewCacheMax 变量决定了 mCachedViews 的最大容量,其最终值是由两部分组成:

void updateViewCacheSize() {
    int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
    mViewCacheMax = mRequestedCacheMax + extraCache;
}

mLayout.mPrefetchMaxCountObservedRecyclerView:预取 篇介绍过,它代表每次预取 ViewHolder 的数量,可以想象到不同的 LayoutManager 值会不同,比如 LinearLayout 每次只需要预取一个 ViewHolder,而 GridLayoutManager 每次需要预取 SpanCountViewHoldermRequestedCacheMax 的值可调用 set 方法设置。所以如果没有特殊修改的话在 LinearLayoutManagermViewCacheMax = 3,在 GridLayoutManagermViewCacheMax = 2 + SpanCount

获取 ViewHolder 缓存

然后先来看 RecyclerView 是如何从缓存中取值的。RecyclerView 在布局时会调用如下方法获取 View

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

tryGetViewHolderForPositionByDeadline() 方法中包含了从缓存中获取 ViewHolder 和创建新 ViewHolder 以及绑定数据的逻辑。

/**
 * @param position 获取 ViewHolder 在列表中的位置
 * @param dryRun 是否从
 * @param deadlineNs 下一帧刷新时间 用于预加载过程
 * @return
 */
RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                                                              boolean dryRun, long deadlineNs) {
    RecyclerView.ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 0) 当前如果处于预布局阶段则从 mChangeScrap 中获取缓存 ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
    }
    // 1) Find by position from scrap/hidden list/cache
    // 1) 从 mAttachedScrap 和 mCachedViews 中获取缓存 ViewHolder
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    }
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
        // 2) 如果开发者设置了 ItemId 则通过 ItemId 获取 ViewHolder
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
            }
        }
        //如果开发者自定义了缓存实现 mViewCacheExtension 则从其获取 ViewHolder
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        //从 RecyclerView 缓存池中获取 ViewHolder
        if (holder == null) { // fallback to pool
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //清理 ViewHolder 中保存的信息以重新绑定数据
                holder.resetInternal();
            }
        }
        // 如果仍然没有获取到 ViewHolder 则只能通过 createViewHolder 创建新的 ViewHolder 了
        if (holder == null) {
            long start = getNanoTime();
            if (deadlineNs != FOREVER_NS
                    && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                // abort - we have a deadline we can't meet
                return null;
            }
            holder = mAdapter.createViewHolder(RecyclerView.this, type);

            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
        }
    }

    // 是否需要重新绑定数据
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    return holder;
}

这里把方法中的代码精简了一部分,更便于理解 RecyclerView 是如何获取 ViewHolder 的。可以看到 RecyclerView 会依次从 mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool 中试图获取缓存的 ViewHolder ,如果未取到则通过 createViewHolder() 新建 ViewHolder。然后再判断 ViewHolder 实例是否需要调用 tryBindViewHolderByDeadline().bindViewHolder() 方法绑定数据。

缓存 ViewHolder

介绍了 RecyclerView 是如何从缓存获取 ViewHolder 的,再来看 ViewHolder 是如何被缓存的,并且这么多缓存方式之间的关系是什么?我们一个个来看。

首先是 mViewCacheExtension、mRecyclerPool,当开发者有自定义缓存需求的可以通过 mViewCacheExtension 替代 mRecyclerPool,所以他们属于功能相同的同一级,默认情况下通过 RecycledViewPool 来缓存从 mCacheViews 中移除的 ViewHolder。使用 RecycledViewPool 中的 ViewHolder 时需要调用 bindViewHolder() 重新绑定数据。

mChangedScrap、mAttachedScrap、mCachedViews 它们之间的关系是什么呢?

首先是 mCacheView,其缓存 ViewHolder 的逻辑在 recycleViewHolderInternal() 方法中。在 预取 篇中介绍过在执行预取逻辑后获取到的 ViewHolder 会通过 recycleViewHolderInternal() 方法进行缓存。

预取逻辑的相关内容可以看这篇RecyclerView:预取

mChangedScrap、mAttachedScrap 仅在 scrapView() 方法被调用时才会缓存 ViewHolder 而它会在 RecyclerView 布局时被调用。可以通过调用 notifyItem***() 方法可以触发 onLayout() 进行断点跟踪其调用逻辑。

到这里可以知道 RecyclerView 缓存的两个重要方法:

  • recycleViewHolderInternal()
  • scrapView()

首先看 recycleViewHolderInternal() 方法:

void recycleViewHolderInternal(ViewHolder holder) {
    // 判断 mCacheViews 中元素是否填充满
    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 添加到 mCacheViews
    mCachedViews.add(targetCacheIndex, holder);
    cached = true;
	}
	if (!cached) {
	    addViewHolderToRecycledViewPool(holder, true);
	    recycled = true;
	}
}

可以看到缓存的 ViewHolder 会先添加到 mCacheViews 集合中,mCacheViews 填满之后,将首位元素移出加入缓存池。

再来看 scrapView() 方法:

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    // ViewHolder 被移除 \ 数据无效 \ 数据未更新 \ 当数据更新时是否使用同一 ViewHolder 执行动画
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

两个方法的代码量都比较少,也容易理解就不过多解释了,看注释就可以。这里重点提一下我只找到了一种会执行 mChangedScrap.add(holder) 逻辑的方式就是调用 notifyItemChange(int) 方法,目标 ViewHolder 会被添加到 mChangedScrap 集合中,这样做的原因下面再讲。

那这两个方法都是如何被触发的呢?

已知的是 预取 是触发 recycleViewHolderInternal() 的方式之一。scrapView() 的触发是在 Layout 阶段,在布局前会首先会调用 scrapView() 把屏幕上所有可见的 ViewHolder 都缓存到 mAttachedScrap / mChangedScrap 集合中,在布局时再从缓存中获取。

在布局过程中调用 addViewInt() 向布局中添加 子View 时会判断如下:

if (holder.isScrap()) {
    holder.unScrap();
}

用于清理 mAttachedScrap 中已复用的 ViewHolder。对于未复用的 ViewHolder在布局结束时最终会执行 recycleViewHolderInternal() 将其添加到 mCachedViews 缓存中。比如由于调用 notifyItemAdd()、notifyItemRemoved() 而被移出屏幕的 ViewHolder

// 布局结束
private void dispatchLayoutStep3() {
    ...
    // 回收未复用的 ViewHolder
    mLayout.removeAndRecycleScrapInt(mRecycler);
    ...
    // 清理 mCangedScrap 缓存集合
    if (mRecycler.mChangedScrap != null) {
    	mRecycler.mChangedScrap.clear();
    }
    ...
}

void removeAndRecycleScrapInt(Recycler recycler) {
    final int scrapCount = recycler.getScrapCount();
    for (int i = scrapCount - 1; i >= 0; i--) {
        recycler.quickRecycleScrapView(scrap);
    }
    recycler.clearScrap();
}

但是如果在 recycler.quickRecycleScrapView() 处断点,会发现代码根本走不到这里。这是因为 RecyclerView 中默认动画的缘故,ItemAnimatormAttachedScrap 中的 ViewHolder 被用于执行动画了,当动画执行完毕会对此 ViewHolder 进行缓存或其他操作。缓存依然是通过 unScrap()recycleViewHolderInternal() 方法。如下设置可以让断点处发挥作用:

recyclerView.itemAnimator = null

到这里 RecyclerView 是如何缓存 ViewHolder 的就清楚了,再来张图加深下印象:

图中 onLayout 过程的缓存逻辑设置了 recyclerView.itemAnimator = null

总结

那现在对于 RecyclerView 缓存就分析结束了,对于开头的问题 RecyclerView 真的是四级缓存吗? 的问题应该也有了答案。RecyclerView三级缓存 但有 四种缓存方式,分如下两种情况:

  • 滑动:执行 mCachedViews + RecycledViewPool 的二级缓存方案,以优化滑动
  • 重布局: 执行 (mAttachedScrap + mChangedScrap) + mCachedViews + RecycledViewPool 的三级缓存方案,以优化重布局过程。

未解疑问

还有一个问题,为什么重布局时需要 mChangeScrap、mAttachedScrap 两个集合来保存数据呢,从代码里来看是因为 mChangeScrap 仅在预布局过程中被使用,当预布局结束,执行真正的布局时 mAttachedScrapmCacheViews 中都获取不到对应的 ViewHolder 而只能从缓存池中获取(或调用 createViewHolder())继而执行数据更新操作。但是为什么不通过 mFlags 字段来区分呢?不是更方便。对于这个问题欢迎同学一起讨论 ^_^