四级缓存
就RV这个东西,它是需要缓存的,就你这个思路很简单,你页面上显示六个view,那我至少得有六个view的这个显示队列,还有随时能用的这个tmp view
Scrap
Scrap 英文原意是「碎片、边角料、废纸」
还在屏幕上,只是temp detach了
只在一次 layout 的过程中存在。 layout 开始前是空的,layout 结束后也是空的,只在中间短暂存在
会触发RV layout的情况
- 调用 notifyXXX(notifyDataSetChanged, notifyItemChanged, notifyItemInserted, …)
- Adapter 换了(setAdapter)
- LayoutManager 换了
- RV 尺寸变了(屏幕旋转、折叠屏展开)
- 主动调 requestLayout()
一次 layout 的完整过程
假设屏幕上现在有 6 个 VH:notifyItemChanged(3)

  1. layout 开始,把屏幕上所有 VH 「暂存」到 Scrap detachAndScrapAttachedViews
  2. 把所有 VH 从屏幕 detach
  3. 按状态分类放入两个 Scrap 容器
    1. mAttachedScrap: 没被 notify 标记的
    2. mChangedScrap: 被 notifyItemChanged(3) 标记了的
  4. 重新 layout,从 Scrap 里拿 VH
  5. mAttachedScrap 里有 直接拿出来贴回屏幕
  6. mAttachedScrap没有,mChangedScrap 里有 :onBindViewHolder 数据变了,要重新填
  7. layout 结束,Scrap 清空
  8. 如果 Scrap 里还有剩余 VH 没被用到(比如数据变少了),它们会被扔进 Pool
    为什么要分两个容器?
    复用规则不一样
    数据没变的 VH : 什么都不用做,直接贴回屏幕
    数据变了的 VH: 需要 onBindViewHolder 重新绑定
    CachedViews
    字面意思:「缓存的 Views]
    滑动过程中,VH 滑出屏幕 → 进 CachedViews 默认容量只有 2
    为什么这里是2?
    CachedViews 的设计目标就是「应对小幅度来回滑动」
  9. 往下滑一点 → 后悔了 → 往上滑回来 2 个够用
  10. 往下滑了很远 → 不打算滑回来 Pool
    如果容量太大,一堆 VH 都带着完整的 position+数据状态躺在内存里,浪费;容量太小,稍微晃一下就失效了。 Google 实测 2 是性价比最好的
    场景:
  11. 用户回滑
  12. CachedViews → 遍历查找 position
  13. 找到 VH: 直接拿回来贴到屏幕 不调 onBindViewHolder onCreateViewHolder
  14. 如果已经在Pool里了,onBindViewHolder
    CachedViews 里面拿到的view 用户回滑时零延迟、零重绘,体验是无缝的
    什么时候需要调?
  15. 分页场景,用户很可能回滑: 比如阅读类 App(看文章、聊天),用户经常上下翻动。把 CachedViews 调大(比如 5-10)能显著提升体验
    什么时候不要调?
    那就那种很少往回滑的了
    Extension
    自定义
    public abstract static class ViewCacheExtension {
    @Nullable
    public abstract View getViewForPositionAndType(
    @NonNull Recycler recycler, int position, int type);

}
他会在查找链路之后紧接
① Scrap → 按 position 找
② CachedViews → 按 position 找
③ Extension → 你自己决定怎么找 ← 这里
④ Pool → 按 viewType 找
⑤ onCreateViewHolder → inflate
但是注意,这里返回的参数值是View,而不是ViewHolder,所以你可以往这一层塞任意来源的 View,RV 会把它当成这一项的 itemView,然后反查这个 View 对应的 ViewHolder(通过getChildViewHolder(view))
用起来长这样
class MyCacheExtension : RecyclerView.ViewCacheExtension() {

  // 你自己维护的 View 缓存
  private val myCache = mutableMapOf<Int, View>()

  override fun getViewForPositionAndType(
      recycler: RecyclerView.Recycler,
      position: Int,
      type: Int
  ): View? {
      // 你自己决定怎么找、找不找得到
      return myCache[position]
  }

}

recyclerView.setViewCacheExtension(MyCacheExtension())
RV 调你时传入 position 和 type,你返回一个 View,或者返回 null 让它继续往下查 Pool
使用限制

  1. 返回的 View 必须已经关联了一个 ViewHolder
    override fun getViewForPositionAndType(…): View? {
    val view = myCache[position] ?: return null

// RV 拿到 view 后会调 getChildViewHolder(view) 反查 VH
// 如果这个 view 没有关联 VH,会崩

return view
}
意味着你塞进缓存的 View 必须是之前通过 onCreateViewHolder 创建过的 itemView,不是随便 inflate 一个就能用
2. 你要自己管理缓存的生命周期
RV 不会帮你清理。Fragment 销毁时、数据变化时,你都要自己决定什么时候清空、挤出
3. 你要自己处理 position 映射
如果数据变了(插入、删除),position 会偏移。你的缓存里 key=5 的 View 可能对应新数据集的 position=6,不处理就用错
4. 你要自己处理 type 匹配
同一个 position 的 viewType 可能变。你缓存时存了 type,RV 查找时会传新 type,不匹配时你应该返回 null
感觉非常的坑啊,拉完了
感觉这个场景非常的有限,就大概是不变类型,不变位置,生命周期简单的这种情况:跨RV复用某一个View实例,固定不变的某个View,比如header之类的。这种主要不咋变,然后这个位置/类型就好说了,然后因为不变,这个生命周期也相对来说好处理,好debug点
比如某些播放器场景,视频 SurfaceView 跟着滚动,不希望被回收重建。但这种一般用 setIsRecyclable(false)+特殊布局管理器处理,不用 Extension
Pool
只知道类型,不知道是第几项
源码简化版
public class RecycledViewPool {
static class ScrapData {
final ArrayList mScrapHeap = new ArrayList<>(); // VH 列表
int mMaxScrap = DEFAULT_MAX_SCRAP; // 容量上限,默认 5
long mCreateRunningAverageNs = 0; // inflate 平均耗时
long mBindRunningAverageNs = 0; // bind 平均耗时
}

SparseArray mScrap = new SparseArray<>(); // key = viewType
}
结构如下:按 viewType 分桶,每个桶有自己的列表和统计信息
RecycledViewPool
├── viewType 1 (APP_LIST_ITEM)
│ └── ScrapData
│ ├── mScrapHeap: [VH, VH, VH] ← 最多 5 个
│ ├── mMaxScrap: 5
│ ├── mCreateRunningAverageNs: 2800000 ← 2.8ms 平均创建时间
│ └── mBindRunningAverageNs: 400000 ← 0.4ms 平均 bind 时间

├── viewType 2 (BIG_WIDGET)
│ └── ScrapData
│ ├── mScrapHeap: [VH, VH]
│ └── …

└── viewType 3 (BANNER_GROUP_CARD)
└── ScrapData
└── …
SparseArray 的 key 就是 viewType。Pool 的查找就是mScrap.get(viewType).mScrapHeap.removeLast()——按类型直接定位桶、弹出一个。这就是为什么 Pool 复用前提是「viewType必须相同」——底层数据结构就是这么分的
然后还有就是Pool 会统计每种 viewType 的创建和 bind 耗时,用于 GapWorker 做预取决策
// RecyclerView 源码简化
void dispatchViewRecycled(ViewHolder holder) {
ScrapData data = getScrapDataForType(holder.mItemViewType);
// 更新移动平均值
data.mCreateRunningAverageNs = runningAverage(
data.mCreateRunningAverageNs, nanosCreateTook);
}

// GapWorker 用这个决定「能不能在两帧之间完成预取」
boolean lastPrefetchFinished(…) {
long avgCreateTime = pool.getRunningAverageCreateTime(viewType);
long timeLeft = deadlineNs - System.nanoTime();
return avgCreateTime < timeLeft; // 时间够再做
}
VH 从 CachedViews 被挤出或滑动时被回收,都会走 putRecycledView。这里会做状态重置