RecyclerVIew 的四级缓存

0 阅读7分钟

Android中RecyclerView的缓存是面试中的场景问题,为了更好的理解四级缓存的作用,今天带大家一起来从源码的角度来看这块内容。

  • 屏内缓存 (mAttachedScrap,mChangedScrap)
  • 离屏缓存 (CachedViews)
  • 自定义缓存 (mViewCacheExtension)
  • 缓存池 (RecycledViewPool)

上述的四级缓存都集中在RecyclerView内部的Recycler类中

public final class Recycler {
    // 屏内缓存
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null; 
    // 离屏缓存
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
       
    private final List<ViewHolder>
            mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
    
    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
    int mViewCacheMax = DEFAULT_CACHE_SIZE;
    // 缓存池
    RecycledViewPool mRecyclerPool;
    // 自定义缓存
    private ViewCacheExtension mViewCacheExtension;
    
    ...

屏内缓存 (mAttachedScrap,mChangedScrap)

跟随源码中的mAttachedScrapmChangedScrap ,主要看两者在哪里add的。可以找到如下方法:

/**
 * Mark an attached view as scrap. (将一个attach View 标记为废弃,这里的attach就是显示在Rv上的)
 *
 * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
 * for rebinding and reuse. Requests for a view for a given position may return a
 * reused or rebound scrap view instance.</p>
 * //"Scrap" 视图仍附加在它们的父级 RecyclerView 上但符合重新绑定和重用的条件。
 * // 对于给定位置的视图请求可能会返回一个重用的或重新绑定的 scrap 视图实例。
 * @param view View to scrap
 */

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view); // 获取ViewHolder
    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"
                    + " recycler pool." + exceptionLabel());
        } // 验证ViewHolder的可用性。 未变化,或可以复用 updated 的 → mAttachedScrap

        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {  // 数据已变化且不能复用 → mChangedScrap

        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        } 
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

明白了两者是在scrapView方法中添加缓存,且mAttachedScrap代表的是未改变的ViewHolder,mChangedScrap代表的是改变的ViewHolder。接下来一起看哪里调用了scrapView方法。

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        if (sVerboseLoggingEnabled) {
            Log.d(TAG, "ignoring view " + viewHolder);
        }
        return;
    }
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
            // ViewHolder 已无效 + 未被移除 + 没有 stable ids
            // 直接回收进 Pool

        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        recycler.scrapView(view);// 调用scrapView进行回收
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

scrapOrRecycleView是怎么被调用的呢?继续往上找可以发现是 detachAndScrapView系列方法调用的。detachAndScrapView方法的调用链如下:

notifyDataSetChanged / notifyItemInserted / requestLayout / 首次显示
  └→ RecyclerView.requestLayout()
       └→ onLayout()
            └→ dispatchLayout()
                 └→ dispatchLayoutStep1()      ← pre-layout
                      └→ mLayout.onLayoutChildren(mRecycler, mState)
                           └→ detachAndScrapAttachedViews(recycler)   ← 在这里
                                └→ scrapOrRecycleView()  × N(每个 child 调一次)

是在onLayoutChildren阶段开始调用。到此第一层缓存的用法已经很明确了,当recyclerView触发onLayout的时候会将所有的View判断塞入mAttachedScrap或者mChangedScrap,再从缓存中取出。如果没变化的ViewHolder直接从mAttachedScrap取出,变化的从mChangedScrap取出在rebind新数据。落实到具体的业务有以下几种情况:

  • 调用notifyItemInserted / notifyItemRemoved

比如列表有 10 个 item,在第 3 个位置插入/删除一条新数据。LayoutManager在onLayoutChildren 开始时,把屏幕上 10 个 child 全部 detach 进 Scrap。然后重新填充布局时,第 1、2、4 - 10 的位置都能从 mAttachedScrap 中命中,直接 attach 。第 3 个位置是新 itemScrap里没有,走 createViewHolder + bindViewHolder。

  • 调用notifyItemChanged

还是一样的例子,列表有 10 个 item,在第 3 个位置修改一条新数据。LayoutManager在onLayoutChildren开始时,把屏幕上 10 个 child 全部 detach 进 Scrap。其中位置 3 因为数据修改进入mChangedScrap,其他进入mAttachedScrap。但是在绑定阶段,位置3会进行rebind。

这也是RecyclerView局部刷新效率高的原因。

总结一下

mAttachedScrap(未变化的 ViewHolder) ——layout 期间 detach,下一轮 layout 直接 attach 回去,不 rebind,因为数据没变,View 也没变。

mChangedScrap(数据变化的 ViewHolder) ——layout 期间 detach,只在 pre-layout 阶段会被查找,取出来后 rebind 新数据,目的是让 LayoutManager 能测量新内容的尺寸来算动画。到了真正的 layout(step2),这个 ViewHolder 会被正常走一遍完整的 bind 流程。


离屏缓存 (mCachedViews)

当列表从屏幕中划出的时候会调用方法recycleViewHolderInternal

void recycleViewHolderInternal(ViewHolder holder) {
    // ... 省略前置检查 ...

    // 检查是否可以被缓存
    boolean cached = false;
    boolean recyclable = false;

    if (!holder.hasAnyOfTheFlags(
            ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
            | ViewHolder.FLAG_UPDATE)) {

        int viewType = holder.getItemViewType();
        final int transientState = holder.itemView.getTransientStateType();

        // 尝试放入 mCachedViews
        if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(
                ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // 淘汰策略:超过容量时,移除最早的
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 淘汰第 0 个(最旧的),将其移入 RecycledViewPool
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            // 如果开启了预取(prefetch),会根据时间戳决定插入位置
            if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // 查找合适的插入点
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }

            // 放入 mCachedViews
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
    }

    if (!cached) {
        // CachedViews 放不下 → 放入 RecycledViewPool
        addViewHolderToRecycledViewPool(holder, ...);
    }
}

可以看到符合条件的holder会放到mCachedViews中。

此外DEFAULT_CACHE_SIZE 可以知道 mCachedViews 的默认大小为2,我们也可以改变这个配置


int mViewCacheMax = DEFAULT_CACHE_SIZE; // 默认size为2

// RecyclerView.java
public void setItemViewCacheSize(int size) {
    mRecycler.setViewCacheSize(size);
}


public void setViewCacheSize(int viewCount) {
    mRequestedCacheMax = viewCount;
    updateViewCacheSize();
}

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

    // first, try the views that can be recycled
    for (int i = mCachedViews.size() - 1;
            i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
        recycleCachedViewAt(i);
    }
}

大体流程如下

用户滑动
  → scrollBy()
    → fill()
      → recycleBy()                    // 清理旧 itemcanViewBeRecycled(child)     // 检查是否滑出可见区域
        → recycler.recycleView(child)  // 回收recycleViewHolderInternal(holder)
            → mCachedViews.add(holder) // 放入缓存layoutChunk()                  // 填充新 itemtryGetViewHolderForPositionByDeadline()
          → 从 mCachedViews 中查找     // 可能命中刚缓存的

自定义缓存 (mViewCacheExtension)

我们可以通过 setViewCacheExtension 设置自定义缓存

void setViewCacheExtension(ViewCacheExtension extension) {
    mViewCacheExtension = extension;
}

在方法 tryGetViewHolderForPositionByDeadline 中查找

// tryGetViewHolderForPositionByDeadline 中
if (holder == null) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
    if (holder != null) {
        // stable id 命中
    }

    // ← ViewCacheExtension 在这里,排在 Scrap 和 CachedViews 之后、Pool 之前
    if (holder == null && mViewCacheExtension != null) {
        final View view = mViewCacheExtension
                .getViewForPositionAndType(this, position, type);
        if (view != null) {
            holder = getChildViewHolder(view);  // 从 View 反查 ViewHolder
            if (holder != null) {
                // 找到了,但后续会走 rebind
            } else {
                // View 没有关联的 ViewHolder,不能用
            }
        }
    }
}

注意,这里查找到的是View而不是ViewHolder,用户需要自己解决缓存的问题。简单来说,如果要使用自定义缓存,则需要开发者自己解决如何存和如何取这两个关键问题,下面给出一个简单的案例:


第一步
class MyCacheExtension extends RecyclerView.ViewCacheExtension {
    // 你自己维护一个容器
    private List<View> myCache = new ArrayList<>();

    @Override
    public View getViewForPositionAndType(Recycler recycler, int position, int type) {
        if (type == TYPE_VIDEO && !myCache.isEmpty()) {
            return myCache.remove(0);  // 有就返回,没有就返回 null
        }
        return null;  // 返回 null 表示"我这里没有,你走默认流程"
    }
}

第二步
class MyRecyclerListener implements RecyclerView.RecyclerListener {
    private MyCacheExtension extension;

    MyRecyclerListener(MyCacheExtension ext) {
        this.extension = ext;
    }

    @Override
    public void onViewRecycled(RecyclerView.ViewHolder holder) {
        if (holder.getItemViewType() == TYPE_VIDEO) {
            extension.myCache.add(holder.itemView);  // 手动存进去
        }
    }
}

第三步
MyCacheExtension extension = new MyCacheExtension();
recyclerView.setViewCacheExtension(extension);
recyclerView.setRecyclerListener(new MyRecyclerListener(extension));

要注意的是,自定义缓存还是会走onBindViewHolder的。自定义缓存通常用于View的复用,onBindViewHolder还是不要执行繁琐的操作。


缓存池(RecyclerViewPool)

RecycledViewPool 是四级缓存里最"重"的一级,也是唯一支持跨 RecyclerView 共享的缓存。

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;   // onCreateViewHolder 平均耗时
        long mBindRunningAverageNs = 0;     // onBindViewHolder 平均耗时
    }

    SparseArray<ScrapData> mScrap = new SparseArray<>();  // key = viewType
    private int mAttachCount = 0;  // 有多少个 RecyclerView 挂在上面
}

RecyclerViewPool内部利用了SparseArray对ViewHolder进行缓存。内部按 viewType 分组,每种 viewType 对应一个 ScrapData,每个 ScrapData 内部是一个 ArrayList,默认最多存 5 个 ViewHolder。SparseArray 比 HashMap 更省内存,适合 key 是 int 的场景。

数据存入流程:

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;

    // 超过上限return
    if (mMaxScrap >= scrapHeap.size()) {
        return;
    }

    // 防御检查:同一个 ViewHolder 不能重复放入
    if (scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }

    scrap.resetInternal();          // 重置 ViewHolder 状态
    scrap.mOwnerRecyclerView = null;
    scrap.mBindingAdapter = null;
    scrapHeap.add(scrap);           // 追加到尾部
}

重点关注一下scrap.resetInternal()方法

void resetInternal() {
    if (sDebugAssertionsEnabled && isTmpDetached()) {
        throw new IllegalStateException("Attempting to reset temp-detached ViewHolder: "
                + this + ". ViewHolders should be fully detached before resetting.");
    }
    // 清空所有的标记,position,id
    mFlags = 0;
    mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
    mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
    clearNestedRecyclerViewIfNotNested(this);
}

这就是为什么从 Pool 取出的 ViewHolder 必须 重新bind。

@Nullable
public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);// 根据ViewType取出scrapData
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) { // 先去最后的ViewHolder
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}

因为最后放入 Pool 的 ViewHolder,它的 View 可能刚刚还在屏幕上渲染过,复用它的渲染成本更低。

总结: RecycledViewPool 实际上是根据ViewType缓存ViewHolder的,减少重新创建View的过程。