262.RecyclerView 缓存复用机制详解

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 }
    }
}
特性AttachedScrapChangedScrap
数据状态数据有效,未改变数据已改变,需更新
匹配方式按 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就像一个快递盒子:

  1. 第一次使用:装了一部iPhone,贴着标签"收件人:张三,地址:北京"
  2. 送达后回收:iPhone被取走了,但盒子上还贴着"张三,北京"的标签
  3. 重新使用:现在要装一台笔记本电脑给"李四,上海"

二、"清空状态"就像撕掉旧标签

为什么要撕掉旧标签?

不撕掉会怎样?

  • 盒子里放的是笔记本电脑(新商品)
  • 但标签还写着"iPhone给张三"(旧信息)
  • 快递员就糊涂了:到底送给谁?送什么?

撕掉旧标签后:

  • 盒子变成了"空白盒子"
  • 没有任何收件人信息
  • 可以重新贴新标签

RecyclerView中的对应关系

  • 盒子 = ViewHolder(那个布局容器)
  • 商品 = View里显示的内容(文字、图片等)
  • 标签 = ViewHolder的元数据(位置、ID、状态等)

"清空状态"就是撕掉旧标签:

  • ViewHolder里的TextView还显示着旧文字(商品还在盒子里)
  • 但是位置、ID这些"标签信息"被清空了
  • ViewHolder变成了"空白容器",可以装新内容

三、"重新绑定"就像贴新标签+换新商品

具体过程

继续用快递盒子比喻:

  1. 收到重新使用的盒子

    • 盒子里可能还有iPhone的包装纸(旧内容)
    • 但没有任何标签(状态已清空)
  2. 重新绑定 = 贴新标签 + 换新商品

    • 贴上新标签:"笔记本电脑给李四,上海"
    • 把iPhone包装纸扔掉,放入笔记本电脑
    • 现在盒子就是全新的了

在RecyclerView中就是:

  1. 从Pool取出ViewHolder

    • TextView里可能还显示着"第5条消息:你好"(旧内容)
    • 但位置、ID等信息已经被清空(没标签)
  2. onBindViewHolder重新绑定

    • 告诉ViewHolder:"你现在是第50个位置"(贴新标签)
    • 把TextView改成"第50条消息:晚安"(换新商品)
    • ViewHolder现在显示的就是第50条消息了、

四、为什么不一开始就把"商品"也清空?

还是用快递盒子比喻

方案A(现在的做法)

  • 只撕掉标签,不清理盒子里的东西
  • 重新使用时直接放入新商品(新商品会覆盖旧商品)
  • 优点:快,省事

方案B(如果清空所有内容)

  • 撕掉标签,还要把盒子里的东西全部清理干净
  • 重新使用时再放入新商品
  • 缺点:浪费时间,因为反正新商品会覆盖旧商品

实际效果

RecyclerView选择方案A是因为:

  • TextView设置新文字时,会自动覆盖旧文字
  • ImageView设置新图片时,会自动覆盖旧图片
  • 所以没必要提前清空,直接覆盖更高效

五、整个过程的生活化总结

场景:你在刷微信聊天记录

  1. 向上滑动

    • 第1条消息"早上好"移出屏幕
    • 这个ViewHolder被收到"回收站"
  2. 继续滑动

    • 需要显示第20条消息"晚安"
    • 从"回收站"拿出一个ViewHolder
  3. 清空状态

    • 擦掉ViewHolder上的"第1条消息"标签
    • 但屏幕上可能还显示着"早上好"
  4. 重新绑定

    • 贴上新标签"第20条消息"
    • 把屏幕内容改成"晚安"
    • 现在显示的就是第20条消息了

关键理解

  • 清空状态:让ViewHolder"忘记"自己之前是谁,变成空白容器
  • 重新绑定:告诉ViewHolder现在要显示什么内容,给它新身份

这样,一个ViewHolder就从"第1条消息"变成了"第20条消息",实现了完美复用!

这就是为什么RecyclerView滑动这么流畅的原因 - 不是每次都做新盒子,而是重复使用旧盒子装新东西!📦✨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值