</>FUNNYGUYZ

RecyclerView 缓存、刷新与稳定性

#RecyclerView#DiffUtil#Payload#StaggeredGrid
← 学习资料优先级:核心

核心原理讲解

RecyclerView 的四级缓存是怎么协同复用的?

你可以把四级缓存想象成四个不同级别的仓库,滑动的每一帧都在它们之间倒腾:

图片 03-1:RecyclerView 四级缓存查找复用决策流程图

  1. 第一级:mAttachedScrap(屏幕内缓存)
    • 作用:临时存放屏幕内正在显示的 ViewHolder。比如列表正在进行局部重新布局(局部刷新),它里面的 ViewHolder 结构没变,只是临时放进去,马上拿出来复用,不需要重新 create 也不用重新 bind
  2. 第二级:mCachedViews(屏幕外一级缓存)
    • 作用:默认容量只有 2 个。存放刚刚滑出屏幕的 ViewHolder。它最强大的一点是连带状态和数据一起保存。如果用户往回滑,直接命中这里,零开销上屏(不 create,不 bind)
  3. 第三级:mViewCacheExtension(自定义扩展)
    • 作用:开发者自定义扩展缓存,基本没人用,直接带过。
  4. 第四级:RecycledViewPool(跨 type 共享池)
    • 作用:按 itemViewType 进行分组存储。滑出屏幕更远的、或者超出 mCachedViews 限制的 ViewHolder 会被丢到这里。丢进来时,它的数据和绑定状态会被清空。取出来复用时,必须要执行 onBindViewHolder 重新绑定数据。它可以被多个不同的 RecyclerView 共享。

DiffUtil 原理与 Payload 局部刷新

  • DiffUtil 的 Myers 算法DiffUtil 底层是用 Myers 差分算法计算两个数据集的差异,生成添加、删除、移动的最优最小步骤。
  • Payload 局部刷新notifyDataSetChanged 无法告诉 RecyclerView 哪些字段发生了变化,通常会引发更大范围的重新绑定和布局工作,也容易造成图片重复加载或动画不自然。通过在 DiffUtil 中实现 getChangePayload(oldItem, newItem),可以只返回发生变化的字段,例如点赞数。更新时,三参数版本的 onBindViewHolder(holder, position, payloads) 可以只更新对应 View,减少不必要的完整绑定;具体是否重新测量或绘制,仍由属性变化和布局过程决定。

图片 03-2:DiffUtil 最小化差异更新与 Payload 局部刷新 bindViewHolder 链路对比图

瀑布流常见崩溃与不一致现象

原生 StaggeredGridLayoutManager 在复杂的列表滑动、多线程数据更新、快速切 Tab 时,经常会抛出 IndexOutOfBoundsException: Inconsistency detected

图片 03-3:StaggeredGridLayoutManager 切换 Tab 导致 span 缓存清空引发 Crash 机制与 BiSerialFlow 修复方案

根本原因在于:当 RecyclerView 正在进行滑动或并发布局计算时,如果 Adapter 的数据源被异步修改了,或者布局管理器保存的列状态(span)与当前真实的位置映射发生了错乱(比如切换 Tab 时缓存被强行清掉了),RecyclerView 的锚点计算就会指向一个不存在或已经被回收的 item,导致致命崩溃。


工程注意事项

  • [!WARNING] 数据源并发修改的坑:在多个协程中同时修改 Adapter 使用的数据源,很容易造成列表状态与更新通知不一致,进而触发 Inconsistency detected 等异常。应当让列表状态由单一流程维护,并在主线程按顺序提交不可变的新列表。

  • [!NOTE] Glide 图片加载防错位:快速滑动时,ViewHolder 被高频复用。如果前一个 item 的图片还没下完,ViewHolder 就已经复用给下一个 item 了,可能会出现图片闪烁或错位。Glide 是通过给 ImageView 设置特定的 Tag(绑定当前的 Request)。当复用时,新的 bind 会触发新的请求,Glide 在发起新请求前会自动取消 ImageView 上的老请求并把占位图设为空,彻底防止错位。