RecyclerView 详解(一)—— 缓存机制
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. 整体架构
RecyclerView 的核心设计是职责分离:
- Adapter:数据 → ViewHolder 的映射(创建 + 绑定)
- LayoutManager:决定 item 怎么摆放(线性、网格、瀑布流)
- Recycler:ViewHolder 的缓存和复用管理
- ItemAnimator:item 增删改的动画
- ItemDecoration:分割线、间距等装饰
- SnapHelper:滑动对齐(如 ViewPager 效果)
RecyclerView
├── Adapter → 提供数据和 ViewHolder
├── LayoutManager → 布局策略
├── Recycler → 缓存管理(四级缓存)
├── ItemAnimator → 动画
├── ItemDecoration → 装饰
└── RecycledViewPool → 跨 RecyclerView 共享缓存2. 四级缓存详解
2.1 第一级:Scrap(屏幕内缓存)
包含两个列表:
mAttachedScrap:存放当前屏幕上的 ViewHolder。在 onLayoutChildren 时,LayoutManager 会先把所有子 View detach 并放入 mAttachedScrap,重新布局时再从中取回。
java
// LinearLayoutManager.onLayoutChildren() 简化流程
void onLayoutChildren(Recycler recycler, State state) {
// 1. 把所有子 View 放入 scrap
detachAndScrapAttachedViews(recycler);
// 2. 重新填充
fill(recycler, layoutState, state);
}
// detachAndScrapAttachedViews 内部
void scrapView(View view) {
ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)) {
holder.addFlags(ViewHolder.FLAG_UPDATE);
mChangedScrap.add(holder); // 有变化的放 changedScrap
} else {
mAttachedScrap.add(holder); // 没变化的放 attachedScrap
}
}mChangedScrap:存放数据已变化的 ViewHolder(调用了 notifyItemChanged)。这些 ViewHolder 在 pre-layout 阶段使用(用于计算动画起始位置),post-layout 时会从 RecycledViewPool 获取新的 ViewHolder 来绑定新数据。
关键区别:
- mAttachedScrap 复用时不需要重新绑定(数据没变)
- mChangedScrap 是为了动画存在的,最终会被回收到 Pool
2.2 第二级:CachedViews(刚离开屏幕的缓存)
java
// 默认大小 2 + prefetch 预取数量
final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();
int mViewCacheMax = DEFAULT_CACHE_SIZE; // 2特点:
- 按 position 精确匹配
- 命中后直接复用,不调用 onBindViewHolder(数据完全一致)
- 容量满时,最老的 ViewHolder 被移到 RecycledViewPool
- 适用场景:用户来回小幅滑动时,刚滑出去的 item 马上滑回来
java
// 查找逻辑
ViewHolder getScrapOrCachedViewForPosition(int position) {
for (int i = 0; i < mCachedViews.size(); i++) {
ViewHolder holder = mCachedViews.get(i);
if (holder.getLayoutPosition() == position // position 必须完全匹配
&& !holder.isInvalid()) {
mCachedViews.remove(i);
return holder;
}
}
return null;
}为什么默认只有 2?因为 CachedViews 保存了完整的 ViewHolder 状态(包括绑定的数据),占用内存较多。2 个刚好覆盖"滑出一两个 item 又滑回来"的场景。
2.3 第三级:ViewCacheExtension(自定义缓存)
java
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}开发者自定义的缓存层,插在 CachedViews 和 RecycledViewPool 之间。实际开发中很少使用,适用于特殊场景(如固定位置的广告 item 需要特殊缓存策略)。
2.4 第四级:RecycledViewPool(回收池)
java
public static class RecycledViewPool {
// 按 viewType 分桶存储
SparseArray<ScrapData> mScrap = new SparseArray<>();
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP; // 默认每种 type 最多 5 个
}
}特点:
- 按 viewType 匹配(不关心 position)
- 命中后需要调用 onBindViewHolder 重新绑定数据
- ViewHolder 被放入 Pool 前会调用
resetInternal()清除所有绑定状态 - 可以跨 RecyclerView 共享
java
// 放入 Pool 时清除状态
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mPayloads = null;
// ViewHolder 变成"干净"的,只保留 itemView
}2.5 完整查找流程源码分析
java
// Recycler.tryGetViewHolderForPositionByDeadline() 简化
ViewHolder tryGetViewHolderForPositionByDeadline(int position, long deadlineNs) {
ViewHolder holder = null;
// 1. 从 mChangedScrap 查找(仅 pre-layout 阶段)
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
}
// 2. 从 mAttachedScrap 和 mCachedViews 按 position 查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position);
}
// 3. 如果 Adapter 有 stableId,按 id 查找
if (holder == null && mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(position));
}
// 4. ViewCacheExtension
if (holder == null && mViewCacheExtension != null) {
View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
// 5. RecycledViewPool
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal(); // 清除状态
// 需要重新绑定
}
}
// 6. 都没有,创建新的
if (holder == null) {
holder = mAdapter.createViewHolder(this, type);
}
// 7. 需要绑定的话,执行绑定
if (needsUpdate) {
mAdapter.bindViewHolder(holder, position);
}
return holder;
}2.6 缓存对比总结
| 缓存层 | 容量 | 匹配方式 | 需要 onBind | 说明 |
|---|---|---|---|---|
| mAttachedScrap | 无限制 | position | 否 | 屏幕内,layout 期间临时存放 |
| mChangedScrap | 无限制 | position | 否 | 数据变化的 item,用于动画 |
| mCachedViews | 2(默认) | position | 否 | 刚滑出屏幕,来回滑动时秒复用 |
| ViewCacheExtension | 自定义 | 自定义 | 自定义 | 很少使用 |
| RecycledViewPool | 5/type | viewType | 是 | 最终回收站,可跨 RV 共享 |
3. 回收流程
ViewHolder 的回收发生在滑动过程中,item 移出屏幕时:
item 滑出屏幕
→ LayoutManager 调用 removeAndRecycleView()
→ Recycler.recycleViewHolderInternal(holder)
→ 先尝试放入 mCachedViews
→ 如果 mCachedViews 满了
→ 把最老的(index 0)移到 RecycledViewPool
→ 当前 holder 放入 mCachedViews 末尾
→ 如果 mCachedViews 没满
→ 直接放入 mCachedViews
→ 如果不能放入 mCachedViews(如被标记 INVALID)
→ 直接放入 RecycledViewPooljava
// 源码简化
void recycleViewHolderInternal(ViewHolder holder) {
boolean cached = false;
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(FLAG_INVALID | FLAG_REMOVED | FLAG_ADAPTER_POSITION_UNKNOWN)) {
// 尝试放入 CachedViews
if (mCachedViews.size() >= mViewCacheMax && !mCachedViews.isEmpty()) {
// 满了,把最老的移到 Pool
recycleCachedViewAt(0);
}
mCachedViews.add(holder);
cached = true;
}
if (!cached) {
// 放入 RecycledViewPool
addViewHolderToRecycledViewPool(holder);
}
}
}4. Scrap 的工作时机
Scrap 缓存容易让人困惑,它只在 layout 过程中使用:
notifyXxx() 被调用
→ requestLayout()
→ onLayoutChildren()
→ Step 1: detachAndScrapAttachedViews() ← 所有子 View 放入 scrap
→ Step 2: fill() ← 从 scrap 中取回需要的
→ Step 3: scrapView 中没被取回的 ← 移到 CachedViews 或 Pool举个例子,屏幕上显示 item 0-9,调用 notifyItemRemoved(3):
- item 0-9 全部 detach 放入 mAttachedScrap
- 重新 fill 时,item 0-2、4-9 从 scrap 取回(position 匹配,不需要 rebind)
- item 3 没被取回,被回收到 Pool
- 底部可能需要新的 item 10,从 Pool 或 create 获取
