RecyclerView:RecycledViewPool(回收池)

1) 它到底是什么(一句话)

RecycledViewPool 是 RecyclerView 的跨位置/跨列表的"冷缓存池" :按 viewType 分桶存放已经创建过但当前未使用 的 ViewHolder 外壳。命中时可跳过 onCreateViewHolder(inflate/构建) ,但仍会执行 onBindViewHolder 重新绑定数据


2) 它在复用链路中的位置(命中顺序)

当 LayoutManager 需要 position=P 的条目,会依次尝试:

  1. Scrap(本帧临时,原地复用,通常免 rebind)

  2. mCachedViews(本地热缓存,命中常免 rebind;setItemViewCacheSize 决定容量)

  3. ViewCacheExtension(可选自定义钩子)

  4. RecycledViewPool按 viewType 取"空壳" ,随后必定 rebind)

  5. 新建(onCreateViewHolder + onBindViewHolder)

命中越靠前成本越低;池命中能大幅减少 创建成本 (inflate/测量),但绑定成本仍在


3) 内部数据结构与取放策略(源码级视角)

  • 池以 viewType -> ScrapData 的映射保存,ScrapData 里有:

    • mScrapHeap: ArrayList:该类型的空闲 ViewHolder 列表
    • mMaxScrap: Int:该类型的容量上限(默认 5
  • 取出:getRecycledView(viewType)

    • 若列表非空,LIFO(取最后一个,命中率更高)返回;随后外部必会 onBindViewHolder(holder, pos, payloads)。
  • 放回:putRecycledView(holder)

    • 若 mScrapHeap.size < mMaxScrap → 直接放入;否则丢弃,等待 GC。
  • 附加/分离:池会记录被多少个 RV 使用(attach/detach),便于共享与清理;clear() 可手动清空。


4) 何时能进池 / 进不了池的情况

  • 可进池:离屏或被移除,且未标记禁止回收。流程:Recycler.recycleView() → Adapter.onViewRecycled(holder)(做清理)→ 入池。

  • 进不了池的常见原因****

    • View 处于 Transient State (正在做动画/临时状态):hasTransientState==true。此时会走 Adapter.onFailedToRecycleView(holder);你可返回 true 强行入池(但必须在 onViewRecycled 里把状态彻底还原)。
    • 调用了 holder.setIsRecyclable(false):临时禁止回收(比如复杂动画期间);调用需成对增减。

5) 和setItemViewCacheSize()的区别(易混点)

  • mCachedViews(热缓存) :同一个 RV 的近距离回滚 命中率高,常可免绑定;容量小(默认 2,可调)。
  • RecycledViewPool(冷缓存)跨位置/跨 RV 复用,仅按 viewType 匹配;命中只省创建,仍要 onBind。
  • StableIds :提升的是 Scrap/Cache 的按 id 直复池依旧只看 viewType,不会因为 stableId 相同而免绑定。

6) 共享池与嵌套/多页复用

  • 可以把多个 RecyclerView 设成同一个池
scss 复制代码
val shared = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VT_BIG, 12)
  setMaxRecycledViews(VT_SMALL, 20)
}
recyclerA.setRecycledViewPool(shared)
recyclerB.setRecycledViewPool(shared)
  • 典型收益场景

    • Tab/分页切换:A 页离屏的卡片直接给 B 页用,避免重新 inflate。

    • 嵌套 RV(纵向列表里横向轮播):父/子列表共享池 + LinearLayoutManager.setInitialPrefetchItemCount(n),显著减少切页白板。

⚠️ 注意:跨 Activity 共享池可能把旧 Activity 的 Context 间接挂住(ViewHolder 是以原始 LayoutInflater/主题创建的)。尽量在同一 Activity 内共享,或确保可接受的主题/Context 策略。


7) 容量调优:什么时候该调、调多少

  • 目标:让重型类型命中池,降低 onCreateViewHolder 次数的尖峰。

  • 建议

    • 统计每类 viewType 可见数量 + 预取数量 ,池上限可先设为该数的 1--2 倍
    • 重型卡片(复杂自定义 View、富媒体):10--20 比较常见;轻量类型保持默认 5 或略增。
    • 数据结构极多样的页面,先优化 viewType 划分(合并仅配色差异的类型),再谈调池。
  • 同时配套

    • setItemViewCacheSize(k) 提高热缓存 → 减少 rebind。
    • 预取(GapWorker)与 setInitialPrefetchItemCount → 错峰创建/绑定。
    • 列表复杂且内存足:Cache 4--8 + Pool(重型类型 10--20) 是常见组合。

8) 生命周期回调要写好(防"脏复用")

  • onViewRecycled(holder) :进入池前最后的清理点

    • 取消图片请求/动画/监听、停止播放器、清空临时状态(勾选/展开/选中/高亮)、复位可复用对象。
  • onFailedToRecycleView(holder) :遇到 transient state,是否允许强行入池;若返回 true,务必保证 onBind 能完全还原 UI。

  • onViewAttached/DetachedFromWindow:仅做轻量显隐处理,不要做重活。


9) 预取(Prefetch)与池的关系

  • 预取会提前请求 将要出现的 position,并按"取 View"流程走一遍:优先从 拿外壳,再 onBind。
  • 这能把创建/绑定摊平到滑动前 ,减少滚动瞬间的主线程尖峰;配合共享池效果更好。

10) 常见坑 & 自检清单

  • 动画/临时状态导致进不了池:是否正确处理 onFailedToRecycleView 与 onViewRecycled 的复位?
  • 脏状态复用 :绑定是否完全数据驱动,不依赖上次残留的 View 状态?
  • 类型过细命中率低:是否合并仅样式差异的类型?
  • 只调池不调缓存/预取:是否一起配置 setItemViewCacheSize 与预取?
  • 跨 Activity 共享池:是否评估了主题/Context 挂靠风险?
  • 池过大:是否监控内存与 onCreateViewHolder 次数的真实下降?(避免"盲目增大只换来内存占用")

11) 实用模板

(A) 页面级配置

scss 复制代码
recyclerView.setItemViewCacheSize(6) // 热缓存,减少 rebind

recyclerView.recycledViewPool.apply {
  setMaxRecycledViews(VT_CARD_HEAVY, 16)
  setMaxRecycledViews(VT_CARD_LIGHT, 20)
}

(B) 共享池(Tab/嵌套)

scss 复制代码
val sharedPool = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VT_BANNER, 10)
  setMaxRecycledViews(VT_PRODUCT, 24)
}
tabs.forEach { it.recycler.setRecycledViewPool(sharedPool) }

(childLayoutManager as LinearLayoutManager).setInitialPrefetchItemCount(6)

(C) 清理与强制回收

kotlin 复制代码
override fun onViewRecycled(h: VH) {
  h.itemView.animate().cancel()
  Glide.with(h.itemView).clear(h.image)
  h.checkbox.setOnCheckedChangeListener(null)
  h.checkbox.isChecked = false
  // 释放播放器/监听等...
}

override fun onFailedToRecycleView(h: VH): Boolean {
  // 若需要强行入池
  h.itemView.animate().cancel()
  return true
}

(D) 观测指标(建议埋点)

  • onCreateViewHolder / onBindViewHolder 次数与耗时分布(P50/P90/P99)
  • 滑动帧时间尖峰(Perfetto/FrameMetrics)
  • 内存占用变化(池上限调整前后对比)

总结

RecycledViewPool = 按 viewType 存外壳、跨位置/跨列表复用,省"创建"不省"绑定"。

想把列表"滑得稳",要合理划分类型、适度调大池上限、共享池+预取错峰、写好 onViewRecycled 清理 ,并配合 Cache/StableIds/DiffUtil 一起用,效果最好。

相关推荐
小时前端3 小时前
现代Web认证体系深度解析:从JWT原理到SSO架构设计
前端·面试
编程岁月5 小时前
java面试-0203-java集合并发修改异常、快速/安全失败原理、解决方法?
java·开发语言·面试
渣哥5 小时前
你以为只是名字不同?Spring 三大注解的真正差别曝光
javascript·后端·面试
洛卡卡了6 小时前
从被动救火到主动预警,接入 Prometheus + Grafana 全流程
后端·面试·架构
洛小豆7 小时前
她问我::is-logged 是啥?我说:前面加冒号,就是 Vue 在发暗号
前端·vue.js·面试
南北是北北8 小时前
android ViewBinding
面试
南北是北北8 小时前
JetPack中常用的设计模式
面试
我是华为OD~HR~栗栗呀9 小时前
华为OD-23届考研-Java面经
java·c++·后端·python·华为od·华为·面试