17、DiffUtil 高效更新:用 DiffUtil 计算新旧数据差异,局部刷新
说实话,做 Android 列表开发这么多年,我最怕听到的一句话就是:「这个列表数据变了,你刷新一下。」
早期我确实这么干——数据一变,直接 notifyDataSetChanged()。简单粗暴,但后果也很明显:整个列表闪一下,用户正在看的条目被顶到不知道哪里去了,性能差的机器上甚至卡顿半秒。我自己用过一些体验很差的 App,列表动不动就整个刷新,那种感觉真的很糟糕。
后来我接触到 DiffUtil,才意识到:原来列表更新可以这么优雅。它就像一个「差异计算器」,帮你精确找出新旧数据之间哪些条目变了、哪些新增了、哪些删除了,然后只刷新那些真正变化的部分。
为什么需要 DiffUtil?
你想想看,一个列表可能有几百条数据。用户只是给其中一条点了个赞,后台返回了全新的数据列表。如果你直接全量刷新,那所有条目都得重新绑定——即使它们的内容根本没变。
这不仅仅是性能问题。全量刷新会导致:
- 列表滚动位置丢失
- 条目闪烁(特别是带图片的)
- 动画效果生硬
- 不必要的视图重建
DiffUtil 就是来解决这个问题的。它内部使用 Eugene W. Myers 的差异算法,能高效计算出两个列表的最小更新操作集。
核心思想:DiffUtil 会生成一个 DiffResult 对象,里面包含了从旧列表到新列表所需的全部变更操作——哪些条目需要移动、哪些需要更新、哪些需要删除或插入。
DiffUtil 的基本用法
使用 DiffUtil 其实不复杂,核心就是实现一个 DiffUtil.Callback。我直接上代码,这样更直观:
class MyDiffCallback(
private val oldList: List<ItemData>,
private val newList: List<ItemData>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
// 判断两个对象是否是同一个 item(通常用 id 判断)
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
// 判断两个 item 的内容是否相同(用于决定是否需要刷新)
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
然后在使用的时候,这样调用:
val diffResult = DiffUtil.calculateDiff(
MyDiffCallback(oldList, newList)
)
adapter.submitList(newList) // 或者用 adapter.setList(newList)
diffResult.dispatchUpdatesTo(adapter)
嗯,这里要注意:dispatchUpdatesTo 必须在 submitList 之后调用,否则适配器里的数据还没更新,DiffUtil 的索引会错乱。我早期踩过这个坑,排查了半天才发现是调用顺序的问题。
深入理解两个核心方法
DiffUtil.Callback 里有几个方法,但最核心的就是 areItemsTheSame 和 areContentsTheSame。这两个方法决定了 DiffUtil 的行为:
| 方法 | 作用 | 返回 true 表示 | 返回 false 表示 |
|---|---|---|---|
| areItemsTheSame | 判断两个位置的 item 是否是同一个对象 | 同一个 item(比如 id 相同) | 不同的 item,需要执行移除或插入 |
| areContentsTheSame | 判断同一个 item 的内容是否变化 | 内容没变,不需要刷新 | 内容变了,需要调用 onBindViewHolder 更新 |
说白了,areItemsTheSame 决定了「是不是同一个东西」,areContentsTheSame 决定了「东西变了没有」。这两个方法配合使用,DiffUtil 就能精确知道哪些条目需要刷新。
我的习惯:areItemsTheSame 里我通常用唯一 ID 做比较,比如数据库的主键。而 areContentsTheSame 里我直接用 data class 的 equals 方法,因为 Kotlin 的 data class 默认实现了结构相等比较,非常方便。
DiffUtil 的性能考量
DiffUtil 的算法复杂度是 O(N + D²),其中 N 是列表总长度,D 是差异数量。对于大多数场景,这个性能完全够用。但如果你列表特别大(比如上千条),建议在后台线程执行计算:
// 在协程或 AsyncTask 中执行
val diffResult = withContext(Dispatchers.Default) {
DiffUtil.calculateDiff(MyDiffCallback(oldList, newList))
}
// 回到主线程更新 UI
withContext(Dispatchers.Main) {
adapter.submitList(newList)
diffResult.dispatchUpdatesTo(adapter)
}
我曾经在一个聊天记录列表里用过 DiffUtil,数据量大概 500 条左右,计算时间基本在 10 毫秒以内,完全不需要切线程。但如果你遇到列表超过 2000 条的情况,还是建议异步处理,避免卡主线程。
注意:DiffUtil 的计算结果依赖于 areItemsTheSame 和 areContentsTheSame 的正确实现。如果这两个方法写错了,DiffUtil 会给出错误的差异结果,导致列表显示异常。我曾经见过一个同事把 areItemsTheSame 写成了比较所有字段,结果每个 item 都被判定为「不同」,DiffUtil 相当于白算了。
ListAdapter:更优雅的封装
其实 Google 官方也意识到 DiffUtil 的调用有点繁琐,所以在 RecyclerView 的扩展库中提供了 ListAdapter。它内部封装了 DiffUtil 的计算和分发逻辑,你只需要实现 AsyncDifferConfig 或者直接重写 areItemsTheSame 和 areContentsTheSame 方法:
class MyListAdapter : ListAdapter<ItemData, MyViewHolder>(object : DiffUtil.ItemCallback<ItemData>() {
override fun areItemsTheSame(oldItem: ItemData, newItem: ItemData): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemData, newItem: ItemData): Boolean {
return oldItem == newItem
}
}) {
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
使用 ListAdapter 后,你只需要调用 adapter.submitList(newList),它就会自动在后台线程计算差异,然后局部刷新。我个人非常推荐这种方式,代码简洁很多,也不容易出错。
DiffUtil 的局限性
任何技术都有它的适用场景,DiffUtil 也不例外:
- 不适合频繁变动的列表:如果列表每秒钟都在变,DiffUtil 的计算开销反而可能比全量刷新还大。
- 不适合超大列表:超过 5000 条时,建议考虑分页加载,而不是一次性全量 Diff。
- 不适合复杂对象比较:如果
areContentsTheSame里做了大量计算(比如比较嵌套对象),反而会拖慢性能。
避坑指南:我曾经在一个实时股票列表里用 DiffUtil,数据每秒刷新一次。结果发现 DiffUtil 的计算时间比全量刷新还长,因为每次都要对比几百只股票的所有字段。后来我改成只对比价格字段,并且用 ListAdapter 的 submitList 做节流,才解决了问题。
知识体系图
下面这张图总结了 DiffUtil 的核心逻辑和它在 RecyclerView 更新流程中的位置:
总结
DiffUtil 是 RecyclerView 高效更新的基石。它通过精确计算新旧数据的差异,实现了局部刷新,避免了全量刷新带来的性能问题和视觉抖动。
在实际项目中,我强烈建议使用 ListAdapter 配合 DiffUtil,这样既能享受自动差异计算的便利,又能保持代码的简洁性。记住:不是所有刷新都需要 notifyDataSetChanged,用 DiffUtil 让你的列表更新更智能、更流畅。
一句话记住:DiffUtil 帮你找到「变」与「不变」,只刷新该刷新的部分。