1) 它到底是什么(一句话)
RecycledViewPool 是 RecyclerView 的跨位置/跨列表的"冷缓存池" :按 viewType 分桶存放已经创建过但当前未使用 的 ViewHolder 外壳。命中时可跳过 onCreateViewHolder(inflate/构建) ,但仍会执行 onBindViewHolder 重新绑定数据。
2) 它在复用链路中的位置(命中顺序)
当 LayoutManager 需要 position=P 的条目,会依次尝试:
-
Scrap(本帧临时,原地复用,通常免 rebind)
-
mCachedViews(本地热缓存,命中常免 rebind;setItemViewCacheSize 决定容量)
-
ViewCacheExtension(可选自定义钩子)
-
RecycledViewPool (按 viewType 取"空壳" ,随后必定 rebind)
-
新建(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 一起用,效果最好。