11、RecyclerView的缓存机制:ViewHolder的复用池,四级缓存
聊到 RecyclerView,大家第一反应肯定是「高性能列表」。但说句实话,很多人用了好几年 RecyclerView,遇到滑动卡顿还是只会加个固定高度或者套一层 if else。嗯,今天我们就来彻底拆解一下它的缓存机制。
我个人习惯把 RecyclerView 的缓存比作一个「四级弹药库」。每一级缓存都有不同的用途和容量,配合起来才能让列表滑动如丝般顺滑。你想想看,如果每次滑动都重新创建 View,那手机早就卡成 PPT 了。
1. 四级缓存概览
先看一张我画的整体结构图,方便你建立全局认知:
这张图我画了好一会儿,就是想让你一眼看明白:RecyclerView 的缓存是分级的,而且每一级都有明确的职责。下面我们逐层拆解。
2. 第一级缓存:AttachedScrap
是什么? mAttachedScrap 是一个 ArrayList,存放的是当前还在屏幕上、但即将被移除或重新绑定的 ViewHolder。
什么时候用? 当调用 notifyItemChanged、notifyItemRemoved 等局部刷新方法时,RecyclerView 会把对应的 ViewHolder 从布局中「拆下来」暂存到 Scrap 中。等布局重新计算完成后,再直接从 Scrap 里取出来复用。
核心特点:
- ViewHolder 仍然和 Adapter 的数据绑定着,不需要重新绑定
- 容量无限制,但只包含当前屏幕可见的 item
- 查找速度最快,因为直接通过 position 匹配
我在项目中遇到过一个问题:用 DiffUtil 做局部刷新时,如果 Adapter 的 getItemId 返回了错误的值,会导致 Scrap 中找不到对应的 ViewHolder,进而触发重新创建。嗯,那性能就大打折扣了。
3. 第二级缓存:Cache(mCachedViews)
Cache 是一个固定大小的 ArrayList,默认容量是 2。它存放的是刚刚滑出屏幕的 ViewHolder。
为什么是 2?因为 RecyclerView 的布局算法需要「前后各预取一个」来保证顺滑。你想想看,用户手指往上滑,屏幕底部要出现新 item,如果 Cache 里刚好有那个位置的 ViewHolder,直接拿来用,连 onBindViewHolder 都不用调。
小技巧: 如果你知道列表的 item 类型单一且布局简单,可以适当调大 mCachedViews 的容量。调用 setItemViewCacheSize(int size) 即可。我一般设成 4-6,效果不错。
Cache 的查找逻辑是按 position 匹配的。如果 Cache 里的 ViewHolder 的 position 和当前需要的 position 一致,就直接返回。否则,这个 ViewHolder 会被降级到 RecycledViewPool。
4. 第三级缓存:ViewCacheExtension
这个比较特殊。它是一个抽象类,默认实现是空的。说白了,Google 留了一个「后门」让你自己实现缓存逻辑。
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
我很少见到有人用这个。除非你有非常特殊的缓存需求,比如某些 item 需要预加载或者持久化缓存。否则,保持默认就好。
注意: 如果你实现了 ViewCacheExtension,它的优先级高于 RecycledViewPool,但低于 Cache。也就是说,RecyclerView 会先查 Scrap,再查 Cache,然后才轮到你的自定义缓存。
5. 第四级缓存:RecycledViewPool
这是最常用也最需要关注的一级缓存。它是一个 HashMap,key 是 viewType,value 是一个 ArrayList 的 ViewHolder 池。
默认情况下,每个 viewType 的池子容量是 5。当 Cache 满了之后,多出来的 ViewHolder 就会被塞进 Pool 里。Pool 里的 ViewHolder 是「干净」的——它们的数据已经被解绑了,需要重新调用 onBindViewHolder。
| 缓存级别 | 数据结构 | 默认容量 | 是否需要重新绑定 | 匹配方式 |
|---|---|---|---|---|
| AttachedScrap | ArrayList | 无限制(屏幕可见) | 否 | 按 position |
| Cache | ArrayList | 2 | 否 | 按 position |
| ViewCacheExtension | 自定义 | 自定义 | 取决于实现 | 自定义 |
| RecycledViewPool | SparseArray<ArrayList> | 每个 viewType 5 个 | 是 | 按 viewType |
我曾经踩过一个坑:某个页面有 3 种 viewType,每种 item 高度差异很大。默认的 Pool 容量 5 根本不够用,导致频繁创建 ViewHolder。后来我把 Pool 容量调到了 15,滑动流畅了很多。
// 调整 RecycledViewPool 容量
RecyclerView.RecycledViewPool pool = recyclerView.getRecycledViewPool();
pool.setMaxRecycledViews(VIEW_TYPE_NORMAL, 15);
pool.setMaxRecycledViews(VIEW_TYPE_HEADER, 10);
pool.setMaxRecycledViews(VIEW_TYPE_FOOTER, 10);
recyclerView.setRecycledViewPool(pool);
6. 缓存查找流程
当 RecyclerView 需要获取一个 ViewHolder 时,它会按照以下顺序查找:
- AttachedScrap:按 position 查找,找到直接返回
- Cache:按 position 查找,找到直接返回
- ViewCacheExtension:调用自定义逻辑
- RecycledViewPool:按 viewType 查找,找到后重新绑定
- 创建新的 ViewHolder:调用 Adapter.createViewHolder()
这个顺序是写死在 Recycler 的 getViewForPosition 方法里的。我建议你去看一下源码,大概 200 行左右,逻辑非常清晰。
7. 避坑指南
我曾经犯过的错:
- 在 Adapter 里用 if-else 判断 viewType,结果忘了在 getItemViewType 里返回正确的值。Pool 里永远找不到匹配的 ViewHolder,每次都重新创建。那性能,惨不忍睹。
- 多个 RecyclerView 共用一个 RecycledViewPool 时,没有考虑 viewType 冲突。两个列表的 viewType 值一样,但布局不同,结果 ViewHolder 被错用了。
- 调大了 Cache 容量但忘了调 Pool 容量。Cache 满了之后 ViewHolder 直接丢弃,Pool 里还是空的。
嗯,这些坑说起来都是细节,但一旦踩到,排查起来真的很费时间。我的建议是:先理解每一级缓存的职责,再根据实际场景调整参数。不要盲目调大容量,内存也是要钱的。
8. 总结
RecyclerView 的四级缓存,说白了就是一套「从快到慢、从精确到模糊」的复用机制。Scrap 最快但只对当前屏幕有效,Cache 次快但容量小,Pool 最慢但容量大。三者配合,才能让列表滑动既省内存又流畅。
你想想看,如果没有这套机制,每次滑动都 inflate 布局 + findViewByid,那手机早就卡成幻灯片了。嗯,理解了缓存,你才算真正懂了 RecyclerView。
公众号:蓝海资料掘金营,微信deep3321