一、四级缓存是谁
从"最近、最贵"到"最远、最便宜"的顺序:
- 
Scrap(临时废料堆)
- 包含:mAttachedScrap、mChangedScrap,以及隐藏但未移出父容器的 child(hidden)。
 - 特征:仍绑定(bound)到当前位置的数据 ,用于同一轮布局快速复用。
 - 来源:布局过程 detach/更新、notifyItemChanged(payload) 等。
 - 生命周期:只跨一次布局/动画帧,不会长期留存。
 
 - 
CachedViews(内存小缓存)
- 字段:mCachedViews,默认容量 2(可调)。
 - 特征:已脱离父容器 ,但仍保持已绑定状态(带 position/type)。
 - 命中快,不需要重新 bind;适合"左右滑动、相邻位置回来的复用"。
 
 - 
ViewCacheExtension(可选自定义层)
- 回调:RecyclerView.ViewCacheExtension#getViewForPositionAndType(...)。
 - 特征:给你一个钩子,可从应用自己的缓存返回 View/ViewHolder。
 - 默认 为 null,少数场景会用(比如复杂卡片跨 RV 共享)。
 
 - 
RecycledViewPool(共享回收池)
- 
字段/类:mRecyclerPool(RecycledViewPool),按 viewType 分桶 ,默认每型容量 5(可调)。
 - 
特征:只存 未绑定 的 ViewHolder;取出后需 重新 bind。
 - 
可在多个 RecyclerView 间共享(例如 ViewPager2 的各页列表)。
 
 - 
 
注:有时你会在文章里看到"屏幕内已附着的 child"也被称作"第 0 级缓存"。严格说它们不在 Recycler 的缓存列表,但"在屏里直接复用"确实是最便宜的一层。
二、取用顺序(getViewForPosition()的核心流程)
当布局需要 position=p 的 item 时,Recycler 按下面先近后远去找 ViewHolder:
- 
Scrap / Hidden / Cached
- 先找 mChangedScrap(处理带 payload/变更的项,pre-layout 用得多)
 - 再找 mAttachedScrap / hidden(已从父移除但还在本次布局可用)
 - 再找 mCachedViews (命中则通常无需 rebind)
 
 - 
ViewCacheExtension(如果注册了)
 - 
RecycledViewPool (按 viewType 取;取到后需要 rebind)
 - 
创建新 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);越下层命中率更稳(容量大/可共享)。
 - 对应优化就围绕"提高上层命中 、合理设置下层容量 、减少新建/重绑"。
 
五、实用优化清单(直接可用)
- 
用对适配器:
- 普通列表:ListAdapter + DiffUtil(减少 notifyDataSetChanged 造成的失效)。
 - ViewPager2 场景:多页列表共享 RecycledViewPool。
 
 
            
            
              scss
              
              
            
          
          val sharedPool = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VIEW_TYPE_ITEM, 50)
}
childRv.setRecycledViewPool(sharedPool)
        - 
调好两级容量:
- setItemViewCacheSize(n) :相邻来回滑可适当增大(如 4~8)。
 - pool.setMaxRecycledViews(type, count) :按屏内最多可见项×2~3 估算。
 
 - 
稳定 ID 提高命中
 
            
            
              kotlin
              
              
            
          
          adapter.setHasStableIds(true)
override fun getItemId(pos: Int) = data[pos].id
        - 
使 Scrap/Cached 命中更精准、动画更平滑。
 - 
避免无谓失效
- 少用全量 notifyDataSetChanged();用 notifyItemRangeChanged 或 DiffUtil。
 - payload 局部刷新,减少重 bind。
 
 - 
复用池跨页面共享(如 Tab/VP2 多 RecyclerView)
- 大幅降低"刚换页就新建一堆 ViewHolder"的抖动。
 
 - 
谨慎使用 ViewCacheExtension
- 只有当你能保证返回的 View 与数据一一对应且生命周期可控时再用。否则交给内建四级就够。
 
 - 
预取(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 的次数、减少卡顿与内存波动。