文章目录
- RecyclerView的缓存复用机制
-
- [1. 引入:ListView的问题](#1. 引入:ListView的问题)
-
- [1.1 没有复用的后果](#1.1 没有复用的后果)
- [1.2 RecyclerView的核心思想](#1.2 RecyclerView的核心思想)
- [2. 四个核心概念](#2. 四个核心概念)
- [3. ViewHolder的作用](#3. ViewHolder的作用)
-
- [3.1 什么是ViewHolder?](#3.1 什么是ViewHolder?)
- [3.2 为什么要有ViewHolder?](#3.2 为什么要有ViewHolder?)
- 哪里看出来用了缓存?
- [4. 四级缓存机制](#4. 四级缓存机制)
-
- [4.1 缓存层级](#4.1 缓存层级)
- [4.2 各层级详解](#4.2 各层级详解)
- [5. 从列表滑动看缓存流程](#5. 从列表滑动看缓存流程)
-
- [5.1 向上滑动时发生了什么?](#5.1 向上滑动时发生了什么?)
- [5.2 回收过程(Item 0滑出)](#5.2 回收过程(Item 0滑出))
- [5.3 获取过程(底部需要Item 8)](#5.3 获取过程(底部需要Item 8))
- [6. 预取机制(Prefetch)](#6. 预取机制(Prefetch))
-
- [6.1 什么是预取?](#6.1 什么是预取?)
- [6.2 优化效果](#6.2 优化效果)
- [7. 代码示例](#7. 代码示例)
-
- [7.1 启用预取(默认开启)](#7.1 启用预取(默认开启))
- [7.2 设置缓存大小](#7.2 设置缓存大小)
- [8. 性能对比](#8. 性能对比)
- [9. 易错点总结](#9. 易错点总结)
- [10. 一句话总结](#10. 一句话总结)
RecyclerView的缓存复用机制
1. 引入:ListView的问题
1.1 没有复用的后果
假设一个列表有1000条数据,每个Item有一个ImageView加载图片。
如果不做任何优化,滑动列表时会不断创建新的View:
java
// 没有复用的情况
public View getView(int position, View convertView, ViewGroup parent) {
// 每次滑动都创建一个新View
View view = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
// 绑定数据...
return view;
}
- 滑动1000条就创建1000个View
- 每个View占用内存,GC频繁
- 滑动卡顿,掉帧严重
1.2 RecyclerView的核心思想
RecyclerView通过四级缓存 + 强制ViewHolder解决了这个问题:
核心思想:View不销毁,只重用
2. 四个核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| ViewType | 每个Item的类型 | 不同款式的衣服 |
| Adapter | 数据 → View的转换器 | 流水线工人 |
| ViewHolder | 持有View的引用,绑定数据 | 衣架 |
| Recycler | 缓存池,管理废弃和复用的View | 仓库 |
3. ViewHolder的作用
3.1 什么是ViewHolder?
ViewHolder是一个持有View引用的容器类,避免每次绑定数据时重复findViewById:
java
// 标准ViewHolder写法
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
ImageView ivIcon;
public MyViewHolder(View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_title);
ivIcon = itemView.findViewById(R.id.iv_icon);
}
}
3.2 为什么要有ViewHolder?
| 操作 | 传统方式(无ViewHolder) | 有ViewHolder |
|---|---|---|
| 创建Item | inflate布局 | inflate布局 |
| 绑定数据 | 每次findViewById | 直接使用缓存的View引用 |
| 性能 | 每次都要查找控件 | 只查找一次 |
不用ViewHolder的话,每次onBindViewHolder都要走一遍findViewById,列表滑动时这个开销就很明显了。
哪里看出来用了缓存?
关键点 :onBindViewHolder 拿到的是已经创建好的 ViewHolder(里面的 View 已经 inflate 过了)。
onBindViewHolder 被调用时,传入的 holder 参数是 RecyclerView 从缓存里取出来的,它的 itemView 早就存在,tvTitle 和 ivIcon 也早就找到了。onBindViewHolder 只负责往这些现成的控件里填新数据。
跟没有缓存的写法对比,一眼就能看出区别:
java
// 没有缓存的写法(每次都要重新找控件)
public void bindView(View view, int position) {
TextView tv = view.findViewById(R.id.tv_title); // 每次都查一遍
tv.setText(dataList.get(position).getTitle());
}
// 有缓存的写法(直接复用)
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.tvTitle.setText(dataList.get(position).getTitle()); // 直接拿,不用查
}
4. 四级缓存机制
4.1 缓存层级
RecyclerView.Recycler
│
┌───────────────────┼───────────────────┐
│ │ │
┌───▼───┐ ┌─────▼─────┐ ┌──────▼──────┐
│一级缓存│ │ 二级缓存 │ │ 三级缓存 │
│ mAttached│ │ mCacheViews│ │mViewCacheExt│
│ Scrap │ │ (大小2) │ │ (默认为2) │
└────────┘ └───────────┘ └─────────────┘
┌───────────────────────────────────────────┐
│ 四级缓存(RecycledViewPool) │
│ 按ViewType缓存,默认5个/类型 │
└───────────────────────────────────────────┘
4.2 各层级详解
| 层级 | 名称 | 大小 | 存放内容 | 使用场景 |
|---|---|---|---|---|
| 1级 | mAttachedScrap | 不固定 | 正在被使用的ViewHolder | 布局过程中 |
| 2级 | mCachedViews | 默认为 2 | 离开屏幕但还保留的View | 滑动回收 |
| 3级 | mViewCacheExtension | 开发者自定义 | 自定义缓存 | 特殊需求 |
| 4级 | RecycledViewPool | 每类 5 个 | 按ViewType缓存的View | 跨RecyclerView共享 |
5. 从列表滑动看缓存流程
5.1 向上滑动时发生了什么?
屏幕可见区域
┌─────────────────────────┐
│ Item 0 (消失) │ ← 顶部Item滑出屏幕
├─────────────────────────┤
│ Item 1 │
│ Item 2 │
│ Item 3 │
│ Item 4 │
├─────────────────────────┤
│ Item 5 │
│ Item 6 │
│ Item 7 │
│ Item 8 (底部新出现) │ ← 底部需要显示新Item
└─────────────────────────┘
5.2 回收过程(Item 0滑出)
Item 0 离开屏幕
│
▼
检查:mCachedViews是否已满?(默认2个)
│
├─ 未满 → 放入mCachedViews(2级缓存)
│ 完整保留View,下次复用无需重新创建
│
└─ 已满 → 把mCachedViews中最旧的移到RecycledViewPool(4级缓存)
View被回收,重新创建时再inflate
5.3 获取过程(底部需要Item 8)
需要显示Item 8
│
▼
1. 从mAttachedScrap查找(1级)
│ 正在使用的View(一般是布局过程中)
▼
2. 从mCachedViews查找(2级)
│ 最近滑出屏幕的View,完整保留
▼
3. 从mViewCacheExtension查找(3级,一般不用)
▼
4. 从RecycledViewPool查找(4级)
│ 按ViewType匹配,需要重新绑定数据
▼
5. 以上都没有 → 创建新的ViewHolder并绑定数据
每次查找都要先找1级,1级没有找2级,2级没有找3级,都没有才找4级或重新创建。
6. 预取机制(Prefetch)
6.1 什么是预取?
RecyclerView在滑动时,会提前准备即将出现的Item:
滑动方向 → ↓
当前可见:
[Item 0] [Item 1] [Item 2] [Item 3]
预取任务:
提前准备 Item 4(下一次滑动可能看到)
在用户手指还没滑到那个位置时,系统就已经提前创建好Item放在缓存里了,等到真正要显示的时候直接拿,而不是现场创建。
6.2 优化效果
| 机制 | 作用 |
|---|---|
| 预取 | 提前创建/绑定View,减少滑动时的卡顿 |
| 异步 | 在空闲时执行,不影响UI主线程 |
7. 代码示例
7.1 启用预取(默认开启)
java
RecyclerView recyclerView = findViewById(R.id.recyclerView);
// 默认已启用,不需要额外配置
7.2 设置缓存大小
java
// 设置mCachedViews大小(默认2)
recyclerView.setItemViewCacheSize(10);
// 设置RecycledViewPool大小
RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
pool.setMaxRecycledViews(viewType, 10);
recyclerView.setRecycledViewPool(pool);
8. 性能对比
| 场景 | 无复用 | 有复用 |
|---|---|---|
| 滑动1000条创建View次数 | 1000次 | 约屏幕可见数+缓存数 |
| 每次绑定数据findViewById | 每次都要 | 只创建时一次 |
| 内存占用 | 越来越高 | 稳定 |
| GC频率 | 高 | 低 |
9. 易错点总结
| 易错点 | 说明 |
|---|---|
| 不同ViewType的View不能复用 | 不同布局无法互相复用 |
| RecycledViewPool不区分ViewType的,只按viewType区分 | 不同类型的View独立缓存 |
| setItemViewCacheSize太大 | 缓存太多会导致内存浪费 |
| 忘记调用adapter.notifyDataSetChanged | 数据变化后界面不更新 |
10. 一句话总结
RecyclerView的缓存复用机制就是四级缓存依次查找:
- mAttachedScrap(当前正在用的)
- mCachedViews(刚滑出去的,完整保留)
- mViewCacheExtension(开发者自定义)
- RecycledViewPool(按类型缓存,需要重新创建)
能复用就不创建,减少频繁布局和内存分配,让滑动更流畅。