浅谈RecyclerView缓存

此篇是浅谈,后续会发布相关源码分析

RecyclerView 是一种在 Android 开发中用于高效展示大量数据列表的视图容器,(ios类似的组件:UICollectionView

缓存的使用过程

当LayoutManager请求RecyclerView提供某个位置的View时,RecyclerView会按以下顺序查找:

  1. 已变更的临时缓存(Changed Scrap) (用于处理数据变更时的动画,如notifyItemChanged()
  2. 已附加的临时缓存(Attached Scrap) (屏幕内尚未完全分离的ViewHolder)
  3. 未移除的隐藏视图 (通过mChildHelper查找隐藏但未移除的View)
  4. 视图缓存(View Cache) (屏幕外已滑出的ViewHolder,默认缓存2个)
  5. 通过稳定ID二次搜索 (若适配器支持稳定ID,会按ID在缓存中精确匹配)
  6. 自定义缓存扩展(ViewCacheExtension) (开发者可自定义缓存策略,但极少使用)
  7. 全局回收池(RecycledViewPool) (跨RecyclerView共享的缓存池,每个ViewType最多存5个)

若上述均失败,调用onCreateViewHolder()创建新实例,必要时通过onBindViewHolder()绑定数据。

缓存层级设计

RecyclerView采用四级缓存机制:

  • 一级缓存(临时缓存):mChangedScrap(动画相关)、mAttachedScrap(屏幕内未分离的ViewHolder)。
  • 二级缓存(滑出屏幕的ViewHolder):mCachedViews,默认容量为2,快速复用最近滑出的Item。
  • 三级缓存(自定义扩展):ViewCacheExtension,开发者可自定义策略(如按特定条件缓存)。
  • 四级缓存(全局池):RecycledViewPool,跨RecyclerView共享,需注意类型(ViewType)隔离

屏幕上显示的视图(A Views)

↓ 滑出屏幕

一级缓存:Scrap / mAttachedScrap

↓ 布局结束后

二级缓存:Cache / mCachedViews

↓ 空间不足 / 不需要暂存(没有设置自定义缓存的情况)

四级缓存:RecycledViewPool

(iOS相对应的view是叫:UITableView和UICollectionView)

RecycledViewPool

介绍

RecycledViewPool 是 RecyclerView.Recycler 的一个内部类:

csharp 复制代码
public static class RecycledViewPool { 
private SparseArray<ArrayList<ViewHolder>> mScrap = 
  new SparseArray<>(); 
  private SparseIntArray mMaxScrap = new SparseIntArray();   
       ... 
    public ViewHolder getRecycledView(int viewType) { 
ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);

每种视图类型都有自己的容量,默认值为 5,但是这个数值可以通过在代码中动态改变。如果你知道某些视图类型的项目非常罕见,每次出现的数量都不会超过一个(比如一个页面的某个首图肯定只有一个),那就应该将该视图类型的项目池大小设置为 1。

且多个RecyclerView可以共同复用同一个RecycledViewPool

应用场景

  1. 滚动过程中,视图超出了 RecyclerView 的边界。

如果 mCachedViews 还有空位会优先放入其中,然后才放入 RecycledViewPool

  1. 数据已更改,因此视图不再可见。当消失动画结束后,才会向池中添加新元素。

比如:某 item 被删除或位置变化,Recycler 启动 item disappear animation(消失动画), 回收时再遵循本地缓存优先、缓存满则入全局池的规则。。

  1. 视图缓存中的某个项目已被更新或删除。
  2. 在查找 ViewHolder 时,我们在缓存或资源中找到了一个位置符合要求的 ViewHolder,但由于视图类型或 ID 错误(如果适配器具有stable ID),该 ViewHolder 并不适用

比如:Recycler 在复用时会按 view type(getItemViewType)和(若启用)stable ID 来查找合适的 ViewHolder。如果找到的 holder 在类型或 ID 上不匹配,就不能直接复用。当找到但不适用的情况下,Recycler 会 把那个不适用的 ViewHolder 回收(即从缓存/候选列表移除) ,随后按回收策略放入 mCachedViews(若有空间且合适)或放入 RecycledViewPool。特别是如果启用了 stable IDs 并且发现 ID 不匹配,Recycler 不能冒然复用以避免错位显示,因此会把该 holder 当作"无用"回收掉。开启 stable ID 会让 Recycler 更敢复用

ID 不匹配会强制回收,不会复用,Stable ID 能减少闪烁和错位,

适合作为"唯一身份",不能随意变化

  1. 布局管理器在布局前添加了一个视图,但在布局后没有添加该视图。比如一些 LayoutManager(或预取/预布局机制)会在 layout/预布局阶段临时 add 一个 view(用于测量或预取),但最终决定不将它作为实际 child 保留。

LayoutManager 决定要不要,Recycler 去缓存里拿,拿不到让 Adapter 造 ViewHolder,回收的进缓存或池子。

每种视图类型的池都采用栈式结构(后进先出)。

当我们向下滚动时,当前显示的项目后面会有一段"尾巴",由缓存的项目和一个池化项目组成。当项目 8 出现在屏幕上时,缓存中找不到合适的 ViewHolder:没有与位置 8 关联的 ViewHolder。因此,我们使用之前位于位置 3 的池化 ViewHolder。当顶部的项目 6 消失时,它会被放入缓存,并将项目 4 推入池中。

当我们开始反方向滚动时,画面就不同了:

ViewCacheExtension

介绍

The idea of ViewCacheExtension is attractive: it's a cache that behaves the way you want. ViewCacheExtension 的理念很有吸引力:它是一个可以按照你期望的方式运行的缓存。

除非你通过setViewCacheExtension()RecyclerView 的方法显式设置它并实现一个简单的接口,否则它不会存在。

应用场景

  • 你有少量"特殊项"(如广告),位置固定、内容稳定、不怎么变化。
  • 这些项分散在列表里,来回滚动时通常会用一个 ViewHolder 在它们之间反复重绑( onBindViewHolder ),造成卡顿。
  • 你希望为每个特殊项保留自己的视图实例,避免重复重绑,提高滚动流畅度。
scala 复制代码
SparseArray<View> specials = new SparseArray<>();
...
//防止将该特殊viewholder放入缓存池中
recyclerView.getRecycledViewPool().setMaxRecycledViews(SPECIAL, 0);

recyclerView.setViewCacheExtension(new RecyclerView.ViewCacheExtension() {
   @Override
   public View getViewForPositionAndType(RecyclerView.Recycler recycler,
                                         int position, int type) {
       return type == SPECIAL ? specials.get(position) : null;
   }
});

...
class SpecialViewHolder extends RecyclerView.ViewHolder {
           ...                
   public void bind(int position) {
       ...
       specials.put(position, itemView);
   }
}

Scrap

  • Scrap 是 RecyclerView 在一次布局过程中优先查找 ViewHolder 的临时容器,与 Pool/View Cache 不同。
  • Scrap 仅在布局(预布局/后布局)期间非空;布局结束后内容会被清理或转移到其他缓存/池。
  • 拆分为两类 Scrap: mAttachedScrap (未请求"替换式"变更动画)与 mChangedScrap (请求替换式变更动画,仅用于预布局)。
mAttachedScrap

在"预布局"和"后布局"都可用;表示旧视图可以继续复用,同一个 ViewHolder 在两阶段都能被取回重新附着。

mChangedScrap

只在"预布局"可用;表示需要"替换式"变更动画,预布局保留旧视图用于记录动画起点,后布局应以"新的 ViewHolder "取代旧视图。

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android