Skip to content

RecyclerView 详解(一)—— 缓存机制

更新: 5/15/2026 字数: 0 字 时长: 0 分钟

1. 整体架构

RecyclerView 的核心设计是职责分离:

  • Adapter:数据 → ViewHolder 的映射(创建 + 绑定)
  • LayoutManager:决定 item 怎么摆放(线性、网格、瀑布流)
  • Recycler:ViewHolder 的缓存和复用管理
  • ItemAnimator:item 增删改的动画
  • ItemDecoration:分割线、间距等装饰
  • SnapHelper:滑动对齐(如 ViewPager 效果)
RecyclerView
├── Adapter          → 提供数据和 ViewHolder
├── LayoutManager    → 布局策略
├── Recycler         → 缓存管理(四级缓存)
├── ItemAnimator     → 动画
├── ItemDecoration   → 装饰
└── RecycledViewPool → 跨 RecyclerView 共享缓存

2. 四级缓存详解

2.1 第一级:Scrap(屏幕内缓存)

包含两个列表:

mAttachedScrap:存放当前屏幕上的 ViewHolder。在 onLayoutChildren 时,LayoutManager 会先把所有子 View detach 并放入 mAttachedScrap,重新布局时再从中取回。

java
// LinearLayoutManager.onLayoutChildren() 简化流程
void onLayoutChildren(Recycler recycler, State state) {
    // 1. 把所有子 View 放入 scrap
    detachAndScrapAttachedViews(recycler);
    // 2. 重新填充
    fill(recycler, layoutState, state);
}

// detachAndScrapAttachedViews 内部
void scrapView(View view) {
    ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)) {
        holder.addFlags(ViewHolder.FLAG_UPDATE);
        mChangedScrap.add(holder);  // 有变化的放 changedScrap
    } else {
        mAttachedScrap.add(holder); // 没变化的放 attachedScrap
    }
}

mChangedScrap:存放数据已变化的 ViewHolder(调用了 notifyItemChanged)。这些 ViewHolder 在 pre-layout 阶段使用(用于计算动画起始位置),post-layout 时会从 RecycledViewPool 获取新的 ViewHolder 来绑定新数据。

关键区别:

  • mAttachedScrap 复用时不需要重新绑定(数据没变)
  • mChangedScrap 是为了动画存在的,最终会被回收到 Pool

2.2 第二级:CachedViews(刚离开屏幕的缓存)

java
// 默认大小 2 + prefetch 预取数量
final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();
int mViewCacheMax = DEFAULT_CACHE_SIZE; // 2

特点:

  • position 精确匹配
  • 命中后直接复用,不调用 onBindViewHolder(数据完全一致)
  • 容量满时,最老的 ViewHolder 被移到 RecycledViewPool
  • 适用场景:用户来回小幅滑动时,刚滑出去的 item 马上滑回来
java
// 查找逻辑
ViewHolder getScrapOrCachedViewForPosition(int position) {
    for (int i = 0; i < mCachedViews.size(); i++) {
        ViewHolder holder = mCachedViews.get(i);
        if (holder.getLayoutPosition() == position  // position 必须完全匹配
                && !holder.isInvalid()) {
            mCachedViews.remove(i);
            return holder;
        }
    }
    return null;
}

为什么默认只有 2?因为 CachedViews 保存了完整的 ViewHolder 状态(包括绑定的数据),占用内存较多。2 个刚好覆盖"滑出一两个 item 又滑回来"的场景。

2.3 第三级:ViewCacheExtension(自定义缓存)

java
public abstract static class ViewCacheExtension {
    public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}

开发者自定义的缓存层,插在 CachedViews 和 RecycledViewPool 之间。实际开发中很少使用,适用于特殊场景(如固定位置的广告 item 需要特殊缓存策略)。

2.4 第四级:RecycledViewPool(回收池)

java
public static class RecycledViewPool {
    // 按 viewType 分桶存储
    SparseArray<ScrapData> mScrap = new SparseArray<>();

    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP; // 默认每种 type 最多 5 个
    }
}

特点:

  • viewType 匹配(不关心 position)
  • 命中后需要调用 onBindViewHolder 重新绑定数据
  • ViewHolder 被放入 Pool 前会调用 resetInternal() 清除所有绑定状态
  • 可以跨 RecyclerView 共享
java
// 放入 Pool 时清除状态
void resetInternal() {
    mFlags = 0;
    mPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mPayloads = null;
    // ViewHolder 变成"干净"的,只保留 itemView
}

2.5 完整查找流程源码分析

java
// Recycler.tryGetViewHolderForPositionByDeadline() 简化
ViewHolder tryGetViewHolderForPositionByDeadline(int position, long deadlineNs) {
    ViewHolder holder = null;

    // 1. 从 mChangedScrap 查找(仅 pre-layout 阶段)
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
    }

    // 2. 从 mAttachedScrap 和 mCachedViews 按 position 查找
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position);
    }

    // 3. 如果 Adapter 有 stableId,按 id 查找
    if (holder == null && mAdapter.hasStableIds()) {
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(position));
    }

    // 4. ViewCacheExtension
    if (holder == null && mViewCacheExtension != null) {
        View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
        if (view != null) {
            holder = getChildViewHolder(view);
        }
    }

    // 5. RecycledViewPool
    if (holder == null) {
        holder = getRecycledViewPool().getRecycledView(type);
        if (holder != null) {
            holder.resetInternal(); // 清除状态
            // 需要重新绑定
        }
    }

    // 6. 都没有,创建新的
    if (holder == null) {
        holder = mAdapter.createViewHolder(this, type);
    }

    // 7. 需要绑定的话,执行绑定
    if (needsUpdate) {
        mAdapter.bindViewHolder(holder, position);
    }

    return holder;
}

2.6 缓存对比总结

缓存层容量匹配方式需要 onBind说明
mAttachedScrap无限制position屏幕内,layout 期间临时存放
mChangedScrap无限制position数据变化的 item,用于动画
mCachedViews2(默认)position刚滑出屏幕,来回滑动时秒复用
ViewCacheExtension自定义自定义自定义很少使用
RecycledViewPool5/typeviewType最终回收站,可跨 RV 共享

3. 回收流程

ViewHolder 的回收发生在滑动过程中,item 移出屏幕时:

item 滑出屏幕
  → LayoutManager 调用 removeAndRecycleView()
    → Recycler.recycleViewHolderInternal(holder)
      → 先尝试放入 mCachedViews
        → 如果 mCachedViews 满了
          → 把最老的(index 0)移到 RecycledViewPool
          → 当前 holder 放入 mCachedViews 末尾
        → 如果 mCachedViews 没满
          → 直接放入 mCachedViews
      → 如果不能放入 mCachedViews(如被标记 INVALID)
        → 直接放入 RecycledViewPool
java
// 源码简化
void recycleViewHolderInternal(ViewHolder holder) {
    boolean cached = false;

    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(FLAG_INVALID | FLAG_REMOVED | FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // 尝试放入 CachedViews
            if (mCachedViews.size() >= mViewCacheMax && !mCachedViews.isEmpty()) {
                // 满了,把最老的移到 Pool
                recycleCachedViewAt(0);
            }
            mCachedViews.add(holder);
            cached = true;
        }

        if (!cached) {
            // 放入 RecycledViewPool
            addViewHolderToRecycledViewPool(holder);
        }
    }
}

4. Scrap 的工作时机

Scrap 缓存容易让人困惑,它只在 layout 过程中使用:

notifyXxx() 被调用
  → requestLayout()
    → onLayoutChildren()
      → Step 1: detachAndScrapAttachedViews()  ← 所有子 View 放入 scrap
      → Step 2: fill()                         ← 从 scrap 中取回需要的
      → Step 3: scrapView 中没被取回的         ← 移到 CachedViews 或 Pool

举个例子,屏幕上显示 item 0-9,调用 notifyItemRemoved(3)

  1. item 0-9 全部 detach 放入 mAttachedScrap
  2. 重新 fill 时,item 0-2、4-9 从 scrap 取回(position 匹配,不需要 rebind)
  3. item 3 没被取回,被回收到 Pool
  4. 底部可能需要新的 item 10,从 Pool 或 create 获取