rv四级缓存学习
四级缓存
就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)
- layout 开始,把屏幕上所有 VH 「暂存」到 Scrap detachAndScrapAttachedViews
- 把所有 VH 从屏幕 detach
- 按状态分类放入两个 Scrap 容器
1. mAttachedScrap: 没被 notify 标记的
2. mChangedScrap: 被 notifyItemChanged(3) 标记了的 - 重新 layout,从 Scrap 里拿 VH
- mAttachedScrap 里有 直接拿出来贴回屏幕
- mAttachedScrap没有,mChangedScrap 里有 :onBindViewHolder 数据变了,要重新填
- layout 结束,Scrap 清空
- 如果 Scrap 里还有剩余 VH 没被用到(比如数据变少了),它们会被扔进 Pool
为什么要分两个容器?
复用规则不一样
数据没变的 VH : 什么都不用做,直接贴回屏幕
数据变了的 VH: 需要 onBindViewHolder 重新绑定
CachedViews
字面意思:「缓存的 Views]
滑动过程中,VH 滑出屏幕 → 进 CachedViews 默认容量只有 2
为什么这里是2?
CachedViews 的设计目标就是「应对小幅度来回滑动」 - 往下滑一点 → 后悔了 → 往上滑回来 2 个够用
- 往下滑了很远 → 不打算滑回来 Pool
如果容量太大,一堆 VH 都带着完整的 position+数据状态躺在内存里,浪费;容量太小,稍微晃一下就失效了。 Google 实测 2 是性价比最好的
场景: - 用户回滑
- CachedViews → 遍历查找 position
- 找到 VH: 直接拿回来贴到屏幕 不调 onBindViewHolder onCreateViewHolder
- 如果已经在Pool里了,onBindViewHolder
CachedViews 里面拿到的view 用户回滑时零延迟、零重绘,体验是无缝的
什么时候需要调? - 分页场景,用户很可能回滑: 比如阅读类 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
使用限制
- 返回的 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
int mMaxScrap = DEFAULT_MAX_SCRAP; // 容量上限,默认 5
long mCreateRunningAverageNs = 0; // inflate 平均耗时
long mBindRunningAverageNs = 0; // bind 平均耗时
}
SparseArray
}
结构如下:按 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
然后还有就是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。这里会做状态重置




