由于项目里大量使用了RecyclerView
控件,在数据加载后进行数据刷新时只是使用了notifyDataSetChanged
方法,该方法也被系统警告说效率不高,碰巧最近在做性能优化,就研究了下DiffUtil
的使用,然后基于AsyncListDiffer
封装了BaseDiffAdapter
和BaseDiffHolder
,在适合场景的页面下把之前的BaseAdapter
和BaseHolder
做了简单替换。下面就来详细介绍下DiffUtil
的使用方法。
DiffUtil
是官方提供的高效计算两个数据集之间差异的工具,在RecyclerView
数据刷新时,可以快速的计算出需要更新的数据集,从而实现高效的数据刷新。常见的使用方法有同步计算的DiffUtil.Callback
, 和异步计算的ListAdapter
和AsyncListDiffer
,使用方式略有不同,下面分别介绍下。
DiffUtil.Callback
DiffUtil.Callback
是DiffUtil
的核心类,用于比较旧数据集和新数据集的元素,并计算它们之间的差异,并且可以与原有的Adapter
写法解耦。它包含四个抽象方法,需要我们自行实现。
1 | public abstract int getOldListSize() // 获取旧数据集的大小。 |
创建自定义的继承DiffUtil.Callback
的类,基本上我们主要关注areItemsTheSame
方法和areContentsTheSame
方法即可,在对比的过程中,areItemsTheSame
方法返回false
则会调用Adapter
对应position
的onCreateViewHolder
,areContentsTheSame
方法返回false
则会调用Adapter
对应position
的onBindViewHolder
三个参数的方法,如果你的Item
里的内容比较丰富可以使用getChangePayload
方法来实现局部刷新。用法如下:
1 |
|
在 Adapter 中处理:
1 |
|
自定义完自己的DiffUtil.Callback
后,就可以把 calculate 后的DiffResult
调用dispatchUpdatesTo
应用到Adapter
里。
1 | List mOldList = mAdapter.getDataList(); |
如果数据量相对较少可以使用DiffUtil.Callback
这种同步计算数据差异的方式,使用起来可以跟项目里现有的Adapter
封装体系解耦,数据量较大且场景合适则需要使用另外两种ListAdapter
和AsyncListDiffer
异步计算的方式。
ListAdapter/AsyncListDiffer
ListAdapter
是RecyclerView.Adapter
的子类,内部集成了AsyncListDiffer
,比较数据差异则需要继承范型类DiffUtil.ItemCallback<T>
,用法跟DiffUtil.Callback
相似,但是DiffUtil.ItemCallback
的areItemsTheSame
方法和areContentsTheSame
方法的参数类型是T
类型,需要自己实现。通过submitList
来提交数据集,submitList
内部会自动计算数据差异,然后应用到Adapter
里。
1 | ListAdapter<YourItem, YourAdapter.ViewHolder> adapter = new ListAdapter<>(new DiffUtil.ItemCallback<YourItem>() { |
这里的areItemsTheSame
和areContentsTheSame
则需要根据具体情况来实现,需要根据不同情况来做针对性的处理。AsyncListDiffer
的用法具体可以参考ListAdapter
内部的实现。
具体使用ListAdapter
还是AsyncListDiffer
需要根据项目的封装情况来决定,拿我这次实践的情况来说,我就是基于AsyncListDiffer
做了BaseDiffAdapter
和BaseDiffHolder
的封装,只需要额外提供一个DiffUtil.ItemCallback
的实现类,就可以跟原本项目里的BaseAdapter
和BaseHolder
做替换。
DiffUtil 算法
DiffUtil
基于Myers
差分算法,通过以下机制实现高效的列表更新:
核心算法:
- 使用编辑图和
Snake
路径计算最短编辑脚本,时间复杂度O(ND)
。 - 双向搜索优化搜索效率,空间复杂度
O(N)
。
源码实现:
calculateDiff()
和diffPartial()
现Myers
算法,生成Snake
序列。computeMoves()
检测移动操作,优化动画。DiffResult
分发更新到 Adapter,触发局部刷新。
优化手段:
- 双向搜索、差异最小化、移动检测、空间优化和异步支持。
- 适配大数据集、复杂动画和低内存设备。
总结
DiffUtil
的使用场景是数据集发生大规模变更时高效的刷新UI
,适合一次性大批量/复杂的数据变化,不适合跟单条数据增删改:notifyItemInserted/Removed/Changed
一起混用,容易导致Adapter
和数据源不同步,使用DiffUtil
处理个别数据变动时需要先使用就数据mOldList
clone 或者新建一个列表之后再对数据进行变动,不然应用到Adapter
后没有效果,所以是否要使用DiffUtil
取决于具体的应用场景和使用后有没有带来性能的提升。