为什么 RecyclerView 采用三层缓存机制

317 阅读6分钟

一、RecyclerView 视图复用机制概述

RecyclerView 的视图复用机制由 RecyclerView.Recycler 类管理,旨在通过缓存 ViewHolder 减少视图创建和垃圾回收的开销,从而提升滚动性能和内存效率。其三层缓存机制包括:

  1. Scrap:临时缓存,用于保存当前布局或数据变更期间的 ViewHolder。
  2. CachedViews:短生命周期缓存,存储最近移除的 ViewHolder(默认最大 2 个)。
  3. RecycledViewPool:共享的视图类型池,按视图类型(viewType)缓存 ViewHolder,供跨 RecyclerView 复用。

这种三层设计是 RecyclerView 高性能和灵活性的核心,下面从设计目标、实现细节和场景分析三个方面解释其必要性。


二、设计目标与三层缓存的必要性

RecyclerView 的视图复用机制针对以下目标进行了优化:

  1. 减少视图创建开销:

    • 创建 View(LayoutInflater.inflate())和执行 findViewById() 是昂贵的操作,尤其在快速滚动或复杂布局时。
    • 三层缓存通过优先复用已有 ViewHolder,避免重复创建视图。
  2. 支持高效的局部更新:

    • RecyclerView 支持动态数据变更(如插入、删除、移动),需要快速重新绑定 ViewHolder 而不是销毁和重建。
    • Scrap 缓存专门为此场景设计,保留临时移除的 ViewHolder。
  3. 优化滚动性能:

    • 快速滚动时,屏幕外的 ViewHolder 需要快速回收和复用。
    • CachedViews 提供快速访问的短时缓存,减少从 RecycledViewPool 或新建 ViewHolder 的开销。
  4. 支持多类型视图和跨 RecyclerView 复用:

    • 复杂列表可能包含多种视图类型(如头部、尾部、普通项),需要按类型管理 ViewHolder。
    • RecycledViewPool 按 viewType 组织缓存,支持跨 RecyclerView 共享,提升内存效率。
  5. 平衡内存与性能:

    • 三层缓存通过不同生命周期和作用范围(临时、短期、长期)平衡内存占用和复用效率。

三、源码分析:三层缓存的实现

以下基于 AndroidX 的 RecyclerView 源码(androidx.recyclerview.widget.RecyclerView 和 Recycler 类),分析三层缓存的具体实现和作用。

  1. Scrap 缓存
  • 定义:

    • Scrap 缓存分为 mAttachedScrap(当前附加到 RecyclerView 的 ViewHolder)和 mChangedScrap(数据变更时的临时 ViewHolder)。
    • 源码位置:RecyclerView.Recycler 中的 ArrayList mAttachedScrap 和 mChangedScrap。
  • 作用:

    • 存储布局过程中或数据变更(notifyItem*())期间的 ViewHolder,供快速复用。
    • Scrap 缓存中的 ViewHolder 仍与数据位置绑定,适合直接重新绑定数据而无需重置。
  • 使用场景:

    • 布局刷新(LayoutManager.onLayoutChildren())时,Scrap 保存当前屏幕上的 ViewHolder。
    • 数据变更(如 notifyItemChanged())时,mChangedScrap 保存需要重新绑定的 ViewHolder。
  • 源码示例:

    java

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
        // 优先从 mAttachedScrap 获取
        for (ViewHolder holder : mAttachedScrap) {
            if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()) {
                if (!dryRun) {
                    mAttachedScrap.remove(holder);
                }
                return holder;
            }
        }
        // 检查 mChangedScrap
        return null;
    }
    
  • 设计意义:

    • Scrap 缓存避免了在布局或数据变更时销毁和重建 ViewHolder,减少性能开销。
    • 它的临时性(仅在单次布局或更新周期内有效)保证了内存占用最小化。
  1. CachedViews 缓存
  • 定义:

    • mCachedViews 是一个固定大小的缓存(默认最大 2 个,DEFAULT_CACHE_SIZE = 2),存储最近移除的 ViewHolder。
    • 源码位置:ArrayList mCachedViews。
  • 作用:

    • 保存刚刚滑出屏幕的 ViewHolder,供快速滚动时复用。
    • 这些 ViewHolder 仍保留原始数据绑定状态(位置、ID 等),适合快速复用而无需重置。
  • 使用场景:

    • 快速滚动时,屏幕外的 ViewHolder 进入 mCachedViews,便于在短时间内滑回时直接复用。
  • 源码示例:

    java

    ViewHolder getViewForPosition(int position) {
        // 尝试从 CachedViews 获取
        for (int i = 0; i < mCachedViews.size(); i++) {
            ViewHolder holder = mCachedViews.get(i);
            if (holder.getLayoutPosition() == position) {
                mCachedViews.remove(i);
                return holder;
            }
        }
        // 未找到则继续尝试其他缓存
        return tryGetViewHolderForPositionByDeadline(position, false, 0);
    }
    
  • 设计意义:

    • CachedViews 提供短生命周期的快速访问缓存,适合高频滚动场景。
    • 固定大小限制(默认 2)避免内存过度占用。
  1. RecycledViewPool 缓存
  • 定义:

    • mRecyclerPool 是一个按视图类型(viewType)组织的缓存池,存储长期可复用的 ViewHolder。
    • 源码位置:RecyclerView.RecycledViewPool 类。
  • 作用:

    • 按视图类型缓存 ViewHolder,支持跨 RecyclerView 共享。
    • ViewHolder 在进入 Pool 前会被重置(resetInternal()),移除数据绑定信息。
  • 使用场景:

    • 当 Scrap 和 CachedViews 无法提供 ViewHolder 时,从 Pool 获取。
    • 适合复杂列表(多视图类型)或嵌套 RecyclerView 的场景。
  • 源码示例:

    java

    ViewHolder getRecycledView(int viewType) {
        SparseArray<ScrapData> scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            return scrapHeap.remove(scrapHeap.size() - 1);
        }
        return null;
    }
    
  • 设计意义:

    • RecycledViewPool 提供长期缓存,支持按需分配 ViewHolder。
    • 按 viewType 组织减少类型不匹配问题,同时支持跨 RecyclerView 复用,优化内存使用。
  1. 获取 ViewHolder 的优先级
  • 在 tryGetViewHolderForPositionByDeadline() 方法中,RecyclerView 按以下顺序尝试获取 ViewHolder:

    1. Scrap(mAttachedScrap 或 mChangedScrap)
    2. CachedViews(mCachedViews)
    3. RecycledViewPool(mRecyclerPool)
    4. 新建(Adapter.createViewHolder())
  • 源码:

    java

    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        // 1. Scrap
        ViewHolder holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) return holder;
        // 2. CachedViews
        holder = getCachedViewForPosition(position);
        if (holder != null) return holder;
        // 3. RecycledViewPool
        holder = getRecycledViewPool().getRecycledView(getItemViewType(position));
        if (holder != null) {
            holder.resetInternal();
            return holder;
        }
        // 4. 新建 ViewHolder
        return mAdapter.createViewHolder(RecyclerView.this, mAdapter.getItemViewType(position));
    }
    

四、为什么需要三层缓存?

三层缓存的设计是 RecyclerView 在性能、内存和灵活性之间权衡的结果,具体原因如下:

  1. 分层满足不同场景的需求:

    • Scrap:处理布局和数据变更的临时需求,保留 ViewHolder 的绑定状态,适合快速重新绑定(如 notifyItemChanged())。
    • CachedViews:优化快速滚动场景,缓存最近移除的 ViewHolder,减少从 Pool 或新建的开销。
    • RecycledViewPool:支持长期缓存和多视图类型场景,适合复杂列表和跨 RecyclerView 复用。
  2. 性能优化:

    • Scrap 和 CachedViews 提供低成本的快速复用,减少访问 RecycledViewPool 或新建 ViewHolder 的开销。
    • 按优先级复用(Scrap → CachedViews → Pool → 新建)确保最优路径获取 ViewHolder。
  3. 内存效率:

    • Scrap 是临时的,仅在布局或更新周期内存在,内存占用极低。
    • CachedViews 容量有限(默认 2),避免过度缓存。
    • RecycledViewPool 按 viewType 组织,允许开发者通过 setMaxRecycledViews() 控制每种类型的缓存数量,平衡内存与性能。
  4. 支持复杂场景:

    • 复杂列表(如多类型视图、嵌套 RecyclerView)需要按 viewType 管理 ViewHolder,RecycledViewPool 提供了这一能力。
    • Scrap 和 CachedViews 针对局部更新和快速滚动优化,而 Pool 支持全局复用,覆盖多种使用场景。
  5. 与 ListView 的对比:

    • ListView 仅有一个简单的缓存机制(mActiveViews 和 mScrapViews),无法区分临时缓存和长期缓存,也不支持多视图类型或跨列表复用。
    • RecyclerView 的三层缓存提供了更细粒度的控制,适应现代复杂 UI 需求。

五、实际场景中的体现

  1. 快速滚动:

    • 用户快速滑动列表时,屏幕外的 ViewHolder 首先进入 mCachedViews,如果缓存满则进入 mRecyclerPool。
    • 当需要显示新 ViewHolder 时,优先从 mCachedViews 获取,减少重置开销。
  2. 数据变更:

    • 调用 notifyItemInserted() 或 notifyItemRemoved() 时,Scrap 缓存保留受影响的 ViewHolder,支持动画和局部刷新。
    • DiffUtil 结合 Scrap 实现最小更新路径,优化性能。
  3. 嵌套 RecyclerView:

    • 嵌套 RecyclerView 场景下,多个 RecyclerView 可共享同一个 RecycledViewPool,减少内存占用。
    • 源码支持:RecyclerView.setRecycledViewPool()。

六、总结

RecyclerView 的三层缓存机制(Scrap、CachedViews、RecycledViewPool)通过分层设计,满足了不同场景下的性能和内存需求:

  • Scrap:临时缓存,优化布局和数据变更。
  • CachedViews:短期缓存,提升快速滚动性能。
  • RecycledViewPool:长期缓存,支持多视图类型和跨 RecyclerView 复用。

这种设计在性能(减少视图创建)、内存(控制缓存规模)和灵活性(支持复杂场景)之间取得了平衡,相比 ListView 的单一缓存机制更加高效和现代化。源码中的优先级复用逻辑(tryGetViewHolderForPositionByDeadline())进一步体现了设计的精妙。