RecyclerView 缓存、刷新与稳定性
#RecyclerView#DiffUtil#Payload#StaggeredGrid
← 学习资料优先级:核心
核心原理讲解
RecyclerView 的四级缓存是怎么协同复用的?
你可以把四级缓存想象成四个不同级别的仓库,滑动的每一帧都在它们之间倒腾:

- 第一级:
mAttachedScrap(屏幕内缓存)- 作用:临时存放屏幕内正在显示的 ViewHolder。比如列表正在进行局部重新布局(局部刷新),它里面的 ViewHolder 结构没变,只是临时放进去,马上拿出来复用,不需要重新 create 也不用重新 bind。
- 第二级:
mCachedViews(屏幕外一级缓存)- 作用:默认容量只有 2 个。存放刚刚滑出屏幕的 ViewHolder。它最强大的一点是连带状态和数据一起保存。如果用户往回滑,直接命中这里,零开销上屏(不 create,不 bind)。
- 第三级:
mViewCacheExtension(自定义扩展)- 作用:开发者自定义扩展缓存,基本没人用,直接带过。
- 第四级:
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,减少不必要的完整绑定;具体是否重新测量或绘制,仍由属性变化和布局过程决定。

瀑布流常见崩溃与不一致现象
原生 StaggeredGridLayoutManager 在复杂的列表滑动、多线程数据更新、快速切 Tab 时,经常会抛出 IndexOutOfBoundsException: Inconsistency detected。

根本原因在于:当 RecyclerView 正在进行滑动或并发布局计算时,如果 Adapter 的数据源被异步修改了,或者布局管理器保存的列状态(span)与当前真实的位置映射发生了错乱(比如切换 Tab 时缓存被强行清掉了),RecyclerView 的锚点计算就会指向一个不存在或已经被回收的 item,导致致命崩溃。
工程注意事项
-
[!WARNING] 数据源并发修改的坑:在多个协程中同时修改 Adapter 使用的数据源,很容易造成列表状态与更新通知不一致,进而触发
Inconsistency detected等异常。应当让列表状态由单一流程维护,并在主线程按顺序提交不可变的新列表。 -
[!NOTE] Glide 图片加载防错位:快速滑动时,ViewHolder 被高频复用。如果前一个 item 的图片还没下完,ViewHolder 就已经复用给下一个 item 了,可能会出现图片闪烁或错位。Glide 是通过给 ImageView 设置特定的
Tag(绑定当前的 Request)。当复用时,新的 bind 会触发新的请求,Glide 在发起新请求前会自动取消 ImageView 上的老请求并把占位图设为空,彻底防止错位。