RecyclerView四级缓存

一、四级缓存是谁

从"最近、最贵"到"最远、最便宜"的顺序:

  1. Scrap(临时废料堆)

    • 包含:mAttachedScrap、mChangedScrap,以及隐藏但未移出父容器的 child(hidden)。
    • 特征:仍绑定(bound)到当前位置的数据 ,用于同一轮布局快速复用。
    • 来源:布局过程 detach/更新、notifyItemChanged(payload) 等。
    • 生命周期:只跨一次布局/动画帧,不会长期留存。
  2. CachedViews(内存小缓存)

    • 字段:mCachedViews,默认容量 2(可调)。
    • 特征:已脱离父容器 ,但仍保持已绑定状态(带 position/type)。
    • 命中快,不需要重新 bind;适合"左右滑动、相邻位置回来的复用"。
  3. ViewCacheExtension(可选自定义层)

    • 回调:RecyclerView.ViewCacheExtension#getViewForPositionAndType(...)。
    • 特征:给你一个钩子,可从应用自己的缓存返回 View/ViewHolder。
    • 默认 为 null,少数场景会用(比如复杂卡片跨 RV 共享)。
  4. RecycledViewPool(共享回收池)

    • 字段/类:mRecyclerPool(RecycledViewPool),按 viewType 分桶 ,默认每型容量 5(可调)。

    • 特征:只存 未绑定 的 ViewHolder;取出后需 重新 bind

    • 可在多个 RecyclerView 间共享(例如 ViewPager2 的各页列表)。

注:有时你会在文章里看到"屏幕内已附着的 child"也被称作"第 0 级缓存"。严格说它们不在 Recycler 的缓存列表,但"在屏里直接复用"确实是最便宜的一层。


二、取用顺序(getViewForPosition()的核心流程)

当布局需要 position=p 的 item 时,Recycler 按下面先近后远去找 ViewHolder:

  1. Scrap / Hidden / Cached

    • 先找 mChangedScrap(处理带 payload/变更的项,pre-layout 用得多)
    • 再找 mAttachedScrap / hidden(已从父移除但还在本次布局可用)
    • 再找 mCachedViews (命中则通常无需 rebind
  2. ViewCacheExtension(如果注册了)

  3. RecycledViewPool (按 viewType 取;取到后需要 rebind

  4. 创建新 ViewHolder:adapter.createViewHolder() → adapter.bindViewHolder()

stableIds 时,还会尝试按 id 在 scrap 里定位,命中率更高。


三、各级的"进出条件"和大小

1) Scrap(mAttachedScrap / mChangedScrap)

  • 进入:布局开始、动画/更新触发时,旧的可复用 child 会被 scrap;notifyItemChanged(payload) 的项会进 mChangedScrap。

  • 移出:同一轮布局内被再次复用;布局结束会清空或移到下一层。

2) CachedViews(mCachedViews)

  • 进入 :item 被移出屏幕且仍然有效(未被 remove/invalid),放入 mCachedViews。

  • 大小:setItemViewCacheSize(n)(默认 2)。

  • 溢出 :超容量时会把最旧的 移入 RecycledViewPool(前提是它依旧有效)。

3) ViewCacheExtension

  • 你控制放与取;Recycler 只调用一次回调,如果你返回视图,它会接管这个 ViewHolder 的后续生命周期。

4) RecycledViewPool

  • 进入:从 mCachedViews 溢出;或 item 被 remove/动画结束复用;或你手动 recycleView()。
  • 大小:按 viewType 设置 setMaxRecycledViews(type, count),默认 5。
  • 共享:childRv.setRecycledViewPool(sharedPool) 可让多个 RV 共用。

四、为什么要四级?------延迟与成本的权衡

层级 是否已绑定 作用域 速度 典型命中场景
Scrap 已绑定 单次布局 ⭐️⭐️⭐️⭐️ 同一帧内位置变化/动画
CachedViews 已绑定 当前 RV ⭐️⭐️⭐️ 相邻页面来回/小范围滚动
ViewCacheExt 取决于你 自定义 变化 自有跨 RV 缓存
Pool 未绑定 跨 RV 共享 ⭐️⭐️ 大范围滚动/跨页面复用
  • 越上层 命中越快(少或不需要 bind);越下层命中率更稳(容量大/可共享)。
  • 对应优化就围绕"提高上层命中合理设置下层容量减少新建/重绑"。

五、实用优化清单(直接可用)

  1. 用对适配器

    • 普通列表:ListAdapter + DiffUtil(减少 notifyDataSetChanged 造成的失效)。
    • ViewPager2 场景:多页列表共享 RecycledViewPool
scss 复制代码
val sharedPool = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VIEW_TYPE_ITEM, 50)
}
childRv.setRecycledViewPool(sharedPool)
  1. 调好两级容量

    • setItemViewCacheSize(n) :相邻来回滑可适当增大(如 4~8)。
    • pool.setMaxRecycledViews(type, count) :按屏内最多可见项×2~3 估算。
  2. 稳定 ID 提高命中

kotlin 复制代码
adapter.setHasStableIds(true)
override fun getItemId(pos: Int) = data[pos].id
  1. 使 Scrap/Cached 命中更精准、动画更平滑。

  2. 避免无谓失效

    • 少用全量 notifyDataSetChanged();用 notifyItemRangeChanged 或 DiffUtil。
    • payload 局部刷新,减少重 bind。
  3. 复用池跨页面共享(如 Tab/VP2 多 RecyclerView)

    • 大幅降低"刚换页就新建一堆 ViewHolder"的抖动。
  4. 谨慎使用 ViewCacheExtension

    • 只有当你能保证返回的 View 与数据一一对应且生命周期可控时再用。否则交给内建四级就够。
  5. 预取(Prefetch)

    • 线性布局:layoutManager.isItemPrefetchEnabled = true(默认开),initialPrefetchItemCount = 6~12。
    • VP2 内嵌 RV:为子 RV 设置 LinearLayoutManager#setInitialPrefetchItemCount,让下页列表先把 ViewHolder/数据准备好。

六、调试与排坑

  • 打开 adb shell setprop log.tag.RecyclerView VERBOSE,看 log 中"recycle / cache / scrap / pool"命中情况。

  • 观察 新建 ViewHolder 次数 (onCreateViewHolder 次数越少越好)与 重绑次数(onBindViewHolder)。

  • 若 mCachedViews 命中低、onCreateViewHolder 频繁:

    • 检查是否大量 notifyDataSetChanged 使缓存失效;
    • viewType 是否过多导致池分桶过细;
    • setItemViewCacheSize 是否太小。
  • 若 OOM:

    • 降低 pool 每型容量;
    • 控制图片解码尺寸与持有时间;
    • 避免在 offscreen 大量保活(offscreenPageLimit 等)。

一句话总结

RecyclerView 的四级缓存 = Scrap → CachedViews → ViewCacheExtension → RecycledViewPool ,取用顺序"近处先找,找不到再远 "。调好 itemViewCacheSize共享 RecycledViewPool ,配合 稳定 ID / DiffUtil / 预取,就能显著降低 onCreateViewHolder/onBindViewHolder 的次数、减少卡顿与内存波动。

相关推荐
汤姆Tom3 小时前
写这么多年CSS,都不知道什么是容器查询?
前端·css·面试
渣哥3 小时前
面试必问:Spring 框架的核心优势,你能说全吗?
javascript·后端·面试
Sailing5 小时前
🔥🔥「别再复制正则了」用 regex-center 一站式管理、校验、提取所有正则
前端·javascript·面试
程序员飞哥5 小时前
真正使用的超时关单策略是什么?
java·后端·面试
南北是北北5 小时前
Coil图片缓存机制
面试
GISer_Jing5 小时前
前端知识详解——HTML/CSS/Javascript/ES5+/Typescript篇/算法篇
前端·javascript·面试
写不来代码的草莓熊5 小时前
vue前端面试题——记录一次面试当中遇到的题(4)
前端·javascript·vue.js·面试
道可到6 小时前
阿里面试原题 java面试直接过06 | 集合底层——HashMap、ConcurrentHashMap、CopyOnWriteArrayList
java·后端·面试
吃饺子不吃馅6 小时前
小明问:要不要加入创业公司?
前端·面试·github