1. 三个核心角色
-
ViewHolder:承载一条 item 视图与元数据(itemViewType、bindingAdapterPosition、mFlags 等)。
-
Recycler(回收器) :负责拿可用的 ViewHolder 和还不用的 ViewHolder(取用与回收的中介)。
-
RecycledViewPool(池) :跨 RecyclerView 共享、按 viewType 分桶的冷缓存,默认每种类型最多 5 个(可调)。
你与它打交道的入口:LayoutManager 在布局/滚动时调用 Recycler.getViewForPosition() 取一个可用的 View,布局完成后不再可见的 View 通过 Recycler.recycleView() 归还。
2. “取一个可用 View”的路径(命中顺序)
当 LayoutManager 需要 position = p 的条目时,会走到 Recycler.getViewForPosition(p) → tryGetViewHolderForPositionByDeadline(),它按以下由近到远的缓存层级查找:
(1) Scrap(当次布局的临时缓存)
-
mAttachedScrap / mChangedScrap:上一帧里还在屏、本帧可复用的 ViewHolder。
-
命中条件:同一个 position(或启用 stableIds 时同一个 id)。
-
特点:不需要重新 inflate,通常也不需要 rebind(除非标记了改变/有 payload)。
(2) Cache(mCachedViews,本地热缓存)
-
尺寸很小(默认 2,setItemViewCacheSize(n) 可调)。
-
存放刚离屏的 ViewHolder,仍携带最近一次的绑定数据。
-
命中同 position(或 id)时,通常免 rebind,可直接复位与复用。
(3) ViewCacheExtension(可选钩子)
-
你能自定义命中策略(很少用,慎用)。
(4) RecycledViewPool(按类型的冷缓存)
-
按 viewType 取一个“空白”ViewHolder(只保留结构,不保证绑定内容)。
-
命中后会调用 onBindViewHolder(holder, position, payloads) 重新绑定。
(5) 完全新建
-
池里也没有 → 调 onCreateViewHolder(parent, viewType) 新建 + 绑定。
命中越靠前,复用成本越低:
Scrap ≈ 原地复用(最省) → Cache(一般省一次 bind) → Pool(要 rebind) → 新建(要 inflate + bind)。
3. “回收一个不用 View”的路径(何时放哪一层)
当 item 滚出屏幕或被移除,LayoutManager 会把它交还 Recycler.recycleView(),随后:
-
若本次布局还可能再用(预测动画/换位等场景),先放进 Scrap。
-
否则尝试放入 mCachedViews(未满则进,满了会把最老的挤到池)。
-
再不行 → 放入 RecycledViewPool 对应 viewType 的桶;超出每桶上限会被真正丢弃,等待 GC。
特殊规则
- Transient state(过渡状态) :ViewCompat.hasTransientState(itemView)=true(如正在播放状态动画)。此时默认不会进池,会回调 Adapter.onFailedToRecycleView(holder),你可以返回 true 强行入池(需确保复用安全)。
- setIsRecyclable(false) :用于临时禁止进池(例如复杂动画进行中),调用次数成对增加/减少。
- StableIds 开启时(setHasStableIds(true)):回收与复用会更大胆地按 id 匹配,减少不必要的 rebind/闪动。
4. 绑定与差量刷新(为什么 payload 很重要)
- 全量绑定:onBindViewHolder(holder, position)。
- 差量绑定:配合 notifyItemChanged(pos, payload),会调用 onBindViewHolder(holder, pos, payloads);若 payloads 非空,你可只更新变更的子视图,避免整 item 重绑(省时、省重绘)。
- DiffUtil / ListAdapter:自动计算增量并带 payload 分发,是现代列表的标配。
5. 预取(Prefetch)与嵌套预取
- GapWorker 会在滚动即将发生/正在发生时,基于 LayoutManager 的预取注册(LayoutPrefetchRegistry)提前请求若干即将出现的位置,走一遍“取 View”流程(带时间截止以避免卡帧)。
- LinearLayoutManager.setInitialPrefetchItemCount(n) :用于嵌套 RV(如横向轮播在纵向列表内),父 RV 会帮子 RV 提前准备 n 个子项,减少切屏白板。
- 预取会创建/绑定 ViewHolder,但是否真正 addView 由布局时机决定;绑定仍在主线程,只是错峰完成,减少滚动时的尖峰成本。
6. 多类型与 Pool 调优
-
池按 viewType 分桶,类型越多池碎片越多;为提升命中率:
- 合理设计 getItemViewType(),避免过度细分(尤其是只配色/微差异不必拆类型)。
- 大型页面共享池:recyclerView.setRecycledViewPool(sharedPool) 并为常见类型 setMaxRecycledViews(viewType, max)。
- 对特别重的 View 类型适当把桶上限调大(如复杂卡片)。
-
小贴士:setItemViewCacheSize(k) 增大本地热缓存,能显著减少短距离滚动的 rebind 频率,但增加内存占用;根据页面复杂度权衡。
7. 生命周期回调(定位问题必看)
-
onViewRecycled(holder):真正进入池前触发,释放资源(取消动画、注销监听、清空临时状态)。
-
onFailedToRecycleView(holder):当视图处于 transient state 无法进池时回调,可返回 true 允许强行入池。
-
onViewAttachedToWindow/…Detached…:可见性边界;不要在这里做耗时。
建议在这些回调里统一清理/还原状态(如取消 Glide 请求、重置 CheckBox),否则会出现“复用脏状态”。
8. 常见问题与对策(工程实践)
-
列表闪烁/位移错乱
- 启用 稳定 ID + 使用 DiffUtil;避免 notifyDataSetChanged() 滥用。
- 不要在 onBind 里依赖 position 作为业务键(用 getItemId() 或数据主键)。
-
复用带脏状态(勾选、展开、播放中…)
- 在 onBind 完全由数据驱动 UI,不要把 UI 状态仅存放在 View;回收前清理。
-
滑动白板/卡顿
- 提升池上限、增加 itemViewCacheSize、开启/调优 预取;
- 把耗时计算放 Dispatchers.Default,图片解码/磁盘/网络放 Dispatchers.IO;
- 约束 item 布局层级与测量成本(能定高就定高,wrap_content 慎用)。
-
多类型命中率低
- 合并相近类型;对重型类型单独调大 setMaxRecycledViews。
-
动画与池冲突
- 动画期间 setIsRecyclable(false),结束后恢复;必要时在 onFailedToRecycleView 返回 true 并在 onViewRecycled 做足清理。
9. 最小可用模板(ListAdapter + Pool 调优)
class CardAdapter : ListAdapter<Card, VH>(DIFF) {
override fun getItemViewType(pos: Int) = when (getItem(pos).style) {
Style.BIG -> 1; Style.SMALL -> 2
}
override fun onCreateViewHolder(p: ViewGroup, t: Int) =
when (t) { 1 -> BigVH.inflate(p); else -> SmallVH.inflate(p) }
override fun onBindViewHolder(h: VH, pos: Int) {
h.bind(getItem(pos)) // 仅用数据驱动UI,不依赖 position
}
override fun onBindViewHolder(h: VH, pos: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) h.bindPartial(getItem(pos), payloads) else onBindViewHolder(h, pos)
}
override fun onViewRecycled(h: VH) { h.cleanup() } // 取消订阅/动画/图片加载
}
// 页面级调优
recyclerView.setItemViewCacheSize(6)
recyclerView.recycledViewPool.apply {
setMaxRecycledViews(1, 10) // BIG
setMaxRecycledViews(2, 15) // SMALL
}
10. 一句话总结
RecyclerView 复用 = 多层缓存(Scrap → Cache → Pool)+ 按类型分桶 + 预取错峰 + 差量绑定。命中越近,成本越低;配合稳定 ID、DiffUtil、合理池大小与干净的绑定/清理逻辑,才能既“滑得动”,又“不断帧”。