1. 基本概念介绍
RecyclerView 是 Android 中用于显示大量数据的高性能列表组件,其核心优势在于强大的缓存复用机制。这套机制通过四级缓存策略,最大化地复用 ViewHolder 对象,避免频繁的视图创建和销毁,从而实现流畅的滚动体验。
1.1 为什么需要缓存机制?
传统的 ListView 在处理大量数据时存在性能问题:
- 频繁调用
findViewById()
查找子视图 - 重复创建和销毁视图对象
- 内存抖动和垃圾回收频繁
RecyclerView 通过 ViewHolder 模式和多级缓存解决了这些问题。
2. 四级缓存机制详解
RecyclerView 的缓存系统采用责任链模式,按优先级依次尝试获取可复用的 ViewHolder:
2.1 第一级:Scrap 缓存
// Scrap 缓存示例概念
class ScrapCache {
private val attachedScrap = mutableListOf<ViewHolder>()
private val changedScrap = mutableListOf<ViewHolder>()
fun getScrapViewHolder(position: Int): ViewHolder? {
// 优先级最高,直接复用,无需重新绑定数据
return attachedScrap.find { it.layoutPosition == position }
}
}
特性 | AttachedScrap | ChangedScrap |
---|---|---|
数据状态 | 数据有效,未改变 | 数据已改变,需更新 |
匹配方式 | 按 position 匹配 | 按 id 或 position 匹配 |
是否重新绑定 | 不需要 | 需要调用 onBindViewHolder |
使用场景 | 布局变化(旋转、resize) | 数据更新(notifyItemChanged) |
动画支持 | 一般不涉及动画 | 支持变化动画 |
生命周期 | 布局过程临时存储 | 布局过程临时存储 |
特点:
- 存储屏幕内正在布局的 ViewHolder
- 分为 AttachedScrap 和 ChangedScrap
- 可直接复用,不需要重新绑定数据
2.2 第二级:Cache 缓存
// Cache 缓存机制 - RecyclerView的二级缓存
class CacheList {
// 存储缓存的ViewHolder列表,按照时间顺序排列(最新的在后面)
private val cachedViews = mutableListOf<ViewHolder>()
// 缓存池的最大容量,默认是2个ViewHolder
// 这意味着最多只能缓存2个刚刚移出屏幕的item
private var cacheSize = 2
/**
* 根据位置查找缓存的ViewHolder
* @param position 要查找的item位置
* @return 如果找到对应位置的ViewHolder就返回,否则返回null
*
* 重点:这里是按position精确匹配的!
* 比如你要显示第5个item,只有缓存中正好有第5个位置的ViewHolder才能用
* 这样的好处是数据完全匹配,不需要重新绑定数据
*/
fun getCachedViewHolder(position: Int): ViewHolder? {
return cachedViews.find { it.layoutPosition == position }
}
/**
* 将ViewHolder回收到缓存列表中
* @param holder 要缓存的ViewHolder(通常是刚刚滑出屏幕的item)
*
* 工作流程:
* 1. 检查缓存是否已满(超过2个)
* 2. 如果满了,就把最老的那个踢出去,送到RecycledViewPool(三级缓存)
* 3. 把新的ViewHolder放到缓存列表末尾
*/
fun recycleToCacheList(holder: ViewHolder) {
// 缓存满了,需要清理空间
if (cachedViews.size >= cacheSize) {
// 移除最老的ViewHolder(列表第一个),把它送到更深层的缓存池
val oldest = cachedViews.removeAt(0)
recycleToPool(oldest) // 这个方法会把ViewHolder送到RecycledViewPool
}
// 把新的ViewHolder加到缓存列表的末尾
// 这样保持了时间顺序:最新的在后面,最老的在前面
cachedViews.add(holder)
}
}
特点:
- 默认大小为 2,可通过
setItemViewCacheSize()
调整 - 存储最近移出屏幕的 ViewHolder
- 保留位置和数据信息,可直接复用
2.3 第三级:ViewCacheExtension
/**
* ViewCacheExtension - RecyclerView的第三级自定义缓存扩展
*
* 这是一个抽象类,允许开发者实现自己的缓存策略
* 在系统的Cache缓存和RecycledViewPool之间插入自定义逻辑
*
* 使用场景:
* - 需要特殊的缓存策略(比如按业务逻辑缓存)
* - 想要缓存一些特殊的View(比如广告位、头部等)
* - 需要跨RecyclerView共享缓存
*/
abstract class ViewCacheExtension {
/**
* 核心方法:根据位置和类型获取缓存的View
*
* @param recycler RecyclerView的回收器,可以用来创建ViewHolder等
* @param position 当前需要显示的item位置
* @param type 当前item的ViewType(通过getItemViewType获得)
* @return 返回缓存的View,如果没有则返回null
*
* 注意:这里返回的是View,不是ViewHolder!
* 系统会自动把View包装成ViewHolder
*/
abstract fun getViewForPositionAndType(
recycler: Recycler,
position: Int,
type: Int
): View?
}
/**
* 自定义缓存扩展的具体实现
* 继承自RecyclerView.ViewCacheExtension(注意这里是RecyclerView的内部类)
*/
class CustomCacheExtension : RecyclerView.ViewCacheExtension() {
/**
* 实现自定义的缓存获取逻辑
*
* @param recycler RecyclerView的回收器对象
* @param position 要显示的item位置(比如第0、1、2...个item)
* @param type item的类型(比如普通item=0,头部=1,广告=2等)
* @return 缓存的View或null
*/
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
type: Int
): View? {
// 自定义缓存逻辑示例:
return if (position % 2 == 0) {
// 如果是偶数位置(0, 2, 4, 6...)
// 使用我们的特殊缓存来获取View
getCustomCachedView(type)
} else {
// 如果是奇数位置(1, 3, 5, 7...)
// 返回null,让系统继续走正常的缓存流程
// (会继续查找RecycledViewPool或创建新的ViewHolder)
null
}
}
/**
* 自定义的缓存获取方法(这个方法需要你自己实现)
*
* @param type ViewType
* @return 缓存的View
*/
private fun getCustomCachedView(type: Int): View? {
// 这里可以实现你的自定义缓存逻辑,比如:
// 1. 从自己维护的缓存Map中获取
// 2. 从数据库中获取预渲染的View
// 3. 从网络缓存中获取
// 4. 使用特殊的View池等等
// 示例实现:
// return myCustomCacheMap[type]?.poll() // 从自定义缓存池取出一个View
return null // 这里只是示例,实际需要你的实现
}
}
2.4 第四级:RecycledViewPool
/**
* RecycledViewPool - RecyclerView的第四级缓存(回收池)
*
* 这是最后一级缓存,也是容量最大的缓存池
* 特点:按ViewType分类存储,不关心position,需要重新绑定数据
*
* 类比:这就像一个大仓库,按商品类型分区存放
* - 电子产品区、服装区、食品区...
* - 取出来的商品需要重新贴标签(重新绑定数据)
*/
class RecycledViewPool {
/**
* 稀疏数组,存储不同ViewType对应的缓存数据
* Key: ViewType (比如 0=普通item, 1=头部, 2=广告, 3=底部)
* Value: ScrapData (每种类型对应的缓存池)
*
* 为什么用SparseArray?
* - ViewType通常是小整数(0,1,2,3...),SparseArray比HashMap更省内存
* - 避免了Integer装箱拆箱的开销
*/
private val scrapHeaps = SparseArray<ScrapData>()
/**
* 每种ViewType对应的缓存数据结构
* 包含一个ViewHolder列表和最大容量限制
*/
class ScrapData {
// 存储ViewHolder的列表(栈结构,后进先出)
val scrapHeap = ArrayList<ViewHolder>()
// 每种ViewType最多缓存5个ViewHolder
// 为什么是5个?经验值,平衡内存占用和缓存命中率
var maxScrap = 5
}
/**
* 根据ViewType获取缓存的ViewHolder
*
* @param viewType View的类型(通过adapter.getItemViewType()获得)
* @return 缓存的ViewHolder,如果没有则返回null
*
* 重要:这里只按ViewType匹配,不关心position!
* 取出来的ViewHolder需要重新调用onBindViewHolder绑定数据
*/
fun getRecycledView(viewType: Int): ViewHolder? {
// 1. 根据ViewType找到对应的缓存池
val scrapData = scrapHeaps[viewType]
// 2. 检查这个类型的缓存池是否有ViewHolder
return if (scrapData?.scrapHeap?.isNotEmpty() == true) {
// 3. 有的话,取出最后一个(栈顶,最新放入的)
// 为什么取最后一个?LIFO策略,最近回收的可能状态更好
scrapData.scrapHeap.removeAt(scrapData.scrapHeap.size - 1)
} else {
// 4. 没有缓存,返回null,系统会创建新的ViewHolder
null
}
}
/**
* 将ViewHolder放入回收池
*
* @param scrap 要回收的ViewHolder(通常来自Cache缓存池溢出的ViewHolder)
*
* 工作流程:
* 1. 获取ViewHolder的类型
* 2. 找到或创建对应类型的缓存池
* 3. 检查缓存池是否已满
* 4. 如果没满,清理ViewHolder并放入缓存池
*/
fun putRecycledView(scrap: ViewHolder) {
// 1. 获取这个ViewHolder的类型
val viewType = scrap.itemViewType
// 2. 获取这个类型对应的缓存池数据
val scrapData = getScrapDataForType(viewType)
// 3. 检查缓存池是否还有空间
if (scrapData.scrapHeap.size < scrapData.maxScrap) {
// 4. 清除ViewHolder内部的数据和状态
// 比如清除position、清除绑定的数据、重置标志位等
scrap.resetInternal() // 相当于"洗干净"重新入库
// 5. 放入缓存池(添加到列表末尾,栈顶)
scrapData.scrapHeap.add(scrap)
}
// 注意:如果缓存池满了,这个ViewHolder就会被直接丢弃(GC回收)
}
/**
* 获取指定ViewType的缓存数据,如果不存在则创建
* (这个方法在原始代码中应该存在,这里补充说明)
*/
private fun getScrapDataForType(viewType: Int): ScrapData {
var scrapData = scrapHeaps[viewType]
if (scrapData == null) {
// 第一次使用这个ViewType,创建新的缓存池
scrapData = ScrapData()
scrapHeaps.put(viewType, scrapData)
}
return scrapData
}
}
Q1: RecyclerView的四级缓存分别在什么时候使用?
A:
- Scrap缓存:布局过程中临时存储的ViewHolder,如数据集变化时
- Cache缓存:用户滚动时,刚移出屏幕的ViewHolder存储在这里
- ViewCacheExtension:开发者自定义场景,如特定位置需要特殊缓存策略
- RecycledViewPool:长距离滚动时,Cache满后的ViewHolder存储池
向下滚动场景
1. item离开屏幕 -> 进入Cache缓存(如果有空位)
2. Cache满了 -> 最老的进入RecycledViewPool,新的进入Cache
3. 需要新item -> 按责任链顺序查找可复用ViewHolder
Q5: Cache缓存和RecycledViewPool的核心区别?
特性 | Cache缓存 | RecycledViewPool |
---|---|---|
索引方式 | 按position精确匹配 | 按viewType分类存储 |
数据保留 | 保留原始数据和状态 | 清空数据,只保留View结构 |
复用效率 | 最高(直接使用) | 中等(需重新绑定) |
适用场景 | 快速往返滚动 | 长距离滚动 |
大小限制 | 默认2个,可调整 | 每类型默认5个 |
// 使用差异示例
// Cache缓存:
val holder = cache.get(position) // 直接使用
// holder.textView.text 仍然是原来的数据
// RecycledViewPool:
val holder = pool.getRecycledView(viewType)
holder.resetInternal() // 清空状态
adapter.onBindViewHolder(holder, newPosition) // 重新绑定
RecyclerView的缓存复用机制是Android性能优化的经典案例,它通过四级缓存和责任链模式,实现了高效的内存管理和流畅的用户体验。理解这套机制不仅有助于写出高性能的列表组件,更能加深对Android架构设计思想的理解。
关键要点:
- 四级缓存各司其职,优先级明确
- 责任链模式保证了系统的可扩展性
- 合理配置缓存参数能显著提升性能
- 源码理解有助于解决实际开发中的性能问题
现在来讲一下这个上面这个代码的清空状态-重新绑定是个什么意思:
一、把ViewHolder想象成"快递盒子"
快递盒子的一生
想象ViewHolder就像一个快递盒子:
- 第一次使用:装了一部iPhone,贴着标签"收件人:张三,地址:北京"
- 送达后回收:iPhone被取走了,但盒子上还贴着"张三,北京"的标签
- 重新使用:现在要装一台笔记本电脑给"李四,上海"
二、"清空状态"就像撕掉旧标签
为什么要撕掉旧标签?
不撕掉会怎样?
- 盒子里放的是笔记本电脑(新商品)
- 但标签还写着"iPhone给张三"(旧信息)
- 快递员就糊涂了:到底送给谁?送什么?
撕掉旧标签后:
- 盒子变成了"空白盒子"
- 没有任何收件人信息
- 可以重新贴新标签
RecyclerView中的对应关系
- 盒子 = ViewHolder(那个布局容器)
- 商品 = View里显示的内容(文字、图片等)
- 标签 = ViewHolder的元数据(位置、ID、状态等)
"清空状态"就是撕掉旧标签:
- ViewHolder里的TextView还显示着旧文字(商品还在盒子里)
- 但是位置、ID这些"标签信息"被清空了
- ViewHolder变成了"空白容器",可以装新内容
三、"重新绑定"就像贴新标签+换新商品
具体过程
继续用快递盒子比喻:
-
收到重新使用的盒子:
- 盒子里可能还有iPhone的包装纸(旧内容)
- 但没有任何标签(状态已清空)
-
重新绑定 = 贴新标签 + 换新商品:
- 贴上新标签:"笔记本电脑给李四,上海"
- 把iPhone包装纸扔掉,放入笔记本电脑
- 现在盒子就是全新的了
在RecyclerView中就是:
-
从Pool取出ViewHolder:
- TextView里可能还显示着"第5条消息:你好"(旧内容)
- 但位置、ID等信息已经被清空(没标签)
-
onBindViewHolder重新绑定:
- 告诉ViewHolder:"你现在是第50个位置"(贴新标签)
- 把TextView改成"第50条消息:晚安"(换新商品)
- ViewHolder现在显示的就是第50条消息了、
四、为什么不一开始就把"商品"也清空?
还是用快递盒子比喻
方案A(现在的做法):
- 只撕掉标签,不清理盒子里的东西
- 重新使用时直接放入新商品(新商品会覆盖旧商品)
- 优点:快,省事
方案B(如果清空所有内容):
- 撕掉标签,还要把盒子里的东西全部清理干净
- 重新使用时再放入新商品
- 缺点:浪费时间,因为反正新商品会覆盖旧商品
实际效果
RecyclerView选择方案A是因为:
- TextView设置新文字时,会自动覆盖旧文字
- ImageView设置新图片时,会自动覆盖旧图片
- 所以没必要提前清空,直接覆盖更高效
五、整个过程的生活化总结
场景:你在刷微信聊天记录
-
向上滑动:
- 第1条消息"早上好"移出屏幕
- 这个ViewHolder被收到"回收站"
-
继续滑动:
- 需要显示第20条消息"晚安"
- 从"回收站"拿出一个ViewHolder
-
清空状态:
- 擦掉ViewHolder上的"第1条消息"标签
- 但屏幕上可能还显示着"早上好"
-
重新绑定:
- 贴上新标签"第20条消息"
- 把屏幕内容改成"晚安"
- 现在显示的就是第20条消息了
关键理解
- 清空状态:让ViewHolder"忘记"自己之前是谁,变成空白容器
- 重新绑定:告诉ViewHolder现在要显示什么内容,给它新身份
这样,一个ViewHolder就从"第1条消息"变成了"第20条消息",实现了完美复用!
这就是为什么RecyclerView滑动这么流畅的原因 - 不是每次都做新盒子,而是重复使用旧盒子装新东西!📦✨