概述
DiffUtil是support-v7:24.2.0新增的工具类,它主要是用来计算两个数据集之间的差异,计算出旧数据集->新数据集的最小变化量,并将其返回。
算法
DiffUtil内部采用ugene W. Myers’s difference 算法。该算法对空间做了优化,并使用O(N)空间来计算两个列表添加和删除的最小操作数,算法的时间复杂度为O(N + D ^ 2)。由于该算法不支持移动的Item,因而Google大牛在此基础上改进支持计算移动的Item。造成的后果就是,DiffUtil需要对结果进行第二遍运算,以便于计算移动的Item,从而更加耗费性能。此时,时间的复杂度为O(N ^ 2), 其中N是添加和删除操作的总数。对于根据约束条件排序的数据集,可以禁用移动Item的检测以提高性能。
用途
DiffUtil主要是与RecyclerView配合使用。其中,由DiffUtil找出每个Item的变化,由RecyclerView.Adapter更新UI。这样的好处就是,在数据集变化时,RecyclerView.Adapter不用无脑的调用notifyDataSetChanged()方法。
核心类
DiffUtil.Callback
DiffUtil.Callback是一个抽象类,在计算两个列表之间的差异时,由DiffUtil回调此类。在该类中,定义了5个抽象方法:
- int getOldListSize(): 获取旧数据集的长度
- int getNewListSize(): 获取新数据集的长度
- boolean areItemsTheSame(int oldItemPosition, int newItemPosition):用来判断 两个对象是否是相同的Item
- boolean areContentsTheSame(int oldItemPosition, int newItemPosition):用来检查 两个item是否含有相同的数据
- Object getChangePayload(int oldItemPosition, int newItemPosition):后续再说
DiffUtil.DiffResult
DiffUtil.DiffResult用于保存DiffUtil计算出的数据集之间的差异信息,其可以将差异信息分配给RecyclerView.Adapter,以便更新UI。
核心方法
- calculateDiff(DiffUtil.Callback cb)
- calculateDiff(DiffUtil.Callback cb, boolean detectMoves)
这两个方法都是用来计算旧数据集->新数据集的最小变化量,并起将其返回。其中,第一个方法是第二个方法的特例,默认开启移动Item的检测:
public static DiffResult calculateDiff(Callback cb) {
return calculateDiff(cb, true);
}
如果禁用移动Item的检测,可以调用第二个方法,并将detectMoves参数设置为false。
简单使用
前文,已经提到DiffUtil主要是与RecyclerView配合使用,以便高效的更新数据集。
创建Bean
data class DiffBean(var name: String, var desc: String) { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false val diff = o as DiffBean? return diff!!.name == name } override fun hashCode(): Int { var result = name?.hashCode() ?: 0 return result } }
创建DiffUtil.Callback
class DiffCallback(private val oldList: List<DiffBean>, private val newList: List<DiffBean>) : DiffUtil.Callback() { /** * 被DiffUtil调用,用来判断 两个对象是否是相同的Item。 * 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等,或者重写equals方法 */ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].name == newList[newItemPosition].name } /** * 老数据集size */ override fun getOldListSize(): Int { return oldList.size } /** * 新数据集size */ override fun getNewListSize(): Int { return newList.size } /** * 被DiffUtil调用,用来检查 两个item是否含有相同的数据 * DiffUtil用返回的信息(true false)来检测当前item的内容是否发生了变化 */ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldBean = oldList[oldItemPosition] val newBean = newList[newItemPosition] // 如果有内容不相同就返回false // if (oldBean.name != newBean.name) { // return false // } if (!TextUtils.equals(oldBean.desc, newBean.desc)) { return false } // //默认两个data内容是相同的 return true } }
在自定义的DiffCallback中,尤其要注意这两个方法:
areItemsTheSame()
方法用来判断Item是否相同。如果Item有唯一的字段,即主键,可以判断两个主键是否相等,就像例子中name
字段作为主键,这么做:oldList[oldItemPosition].name == newList[newItemPosition].name
。当然,也可以重写equals方法(),即TextUtils.equals(oldList[oldItemPosition], newList[newItemPosition])
.其核心点就是 当两个Item相同时,返回true,否则 返回false。areContentsTheSame()
用来判断Item是否相同的内容。如果Item的字段非常多,是否需要全部都需要比较呢?个人觉得,只需要对UI显示有影响的字段做比较即可,并不需要所有的字段都做出判断,不仅影响效率,也没啥用。
创建Apdater
class DiffAdapter : RecyclerView.Adapter<DiffAdapter.ViewHolder>() { val mList: MutableList<DiffBean> = mutableListOf() *** *** fun setData(list: List<DiffBean>) { //利用DiffUtil.calculateDiff()方法,传入一个规则DiffUtil.Callback对象,和是否检测移动item的 boolean变量,得到DiffUtil.DiffResult 的对象 val result: DiffUtil.DiffResult = DiffUtil.calculateDiff(DiffCallback(mList, list), true) //利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年 result.dispatchUpdatesTo(this) // 更新数据集,必须放在dispatchUpdatesTo之后,否则getChangePayload()将无效 // 因为在getChangePayload()还需要对新旧数据集中的Item比较 mList.clear() mList.addAll(list) } *** }
在setData()方法中,DiffUtil在调用calculateDiff()计算新旧数据集差异时,传递了两个参数,第一个参数为DiffUtil.Callback对象,第二个参数用来设置在计算时是否禁用检测移动的Item。当改为false时,将禁用检测移动的Item,此时效率更高。如果数据集已经根据给定条件进行排序,第二个参数可以设置为false,以提高计算的效率。
更新数据集
mList.apply { add(DiffBean("A", "这是A")) add(DiffBean("B", "这是B")) add(DiffBean("C", "这是C")) add(DiffBean("D", "这是D")) add(DiffBean("E", "这是E")) } mAdapter.setData(mList)
可以看来,当使用DiffUtill和RecyclerView使用,再也不用无脑的调用notifyDataSetChanged()方法来更新UI。而,所看不见的是,更新UI的效率更高了,那就是源自DiffUtil内部的ugene W. Myers’s difference 算法。
UI是如何更新的?
当数据集更新时,RecyclerView.Adapter并没有调用notifyDataSetChanged()方法, UI确更新了?这里有点疑惑。在使用DiffUtil的过程中,与RecyclerView.Adapter有交集的只有DiffUtil.DiffResult的dispatchUpdatesTo()方法。这个方法是用来将DiffUtil计算出的由旧数据集->新数据集的最小量分配给RecyclerView.Adapter。接下来,跟踪下它的源码:
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}
在dispatchUpdatesTo(RecyclerView.Adapter)方法中又调用了dispatchUpdatesTo(ListUpdateCallback)方法,此时,将我们传递过去的Adapter创建了AdapterListUpdateCallback对象:
public final class AdapterListUpdateCallback implements ListUpdateCallback {
private final RecyclerView.Adapter mAdapter;
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}
AdapterListUpdateCallback是ListUpdateCallback的实现类,其内调用了Adapter一些列更新UI的方法。而在dispatchUpdatesTo(ListUpdateCallback)方法中,根据更新操作,即旧数据集->新数据集的操作,分配给指定的回调,从而更新UI。
这也就是说,DiffUtil不仅可以跟RecyclerView使用,还可以与ListView,或者是其他的列表,一起使用。我们只需自定义ListUpdateCallback,实现它的4个方法,然后调用dispatchUpdatesTo(ListUpdateCallback)方法,将更新操作分发出去即可。至于更新操作分发的细节,有兴趣的可以查看dispatchUpdatesTo(ListUpdateCallback)方法的源码。
getChangePayload
暂且不谈getChangePayload()方法,先来看RecyclerView中的一个方法:
public void onBindViewHolder(@NonNull VH holder, int position,
@NonNull List<Object> payloads) {
onBindViewHolder(holder, position);
}
对于该方法,官方是这么介绍的:
由RecyclerView调用以在指定位置显示数据. 此方法 更新ViewHolder的itemView的内容以反映给指定位置的Item的变化。
请注意: 与ListView不同的是,如果给定位置的item的数据集变化了,RecyclerView不会再次调用这个方法,除非item本身失效了invalidated ) 或者新的位置不能确定。 由于这个原因,在这个方法里,你应该只使用 postion参数 去获取相关的数据item,而且不应该>去保持 这个数据item的副本。如果稍后需要Item的 postion,比如,在点击事件监听中,使用 ViewHolder.getAdapterPosition(),它>能提供 更新后的位置。
部分绑定 vs完整绑定
payloads 参数 是一个从(notifyItemChanged(int, Object)或notifyItemRangeChanged(int, int, Object))里得到的合并list。 如果payloads list不为空,ViewHolder当前与旧数据绑定,而Adapter可以使用payload 信息运行高效的局部更新。
如果payload为null,Adapter必须执行完整绑定。Adapter不应该认为onBindViewHolder()会接收到
notify
方法传递的有效信息。例如,当View没有attached 在屏幕上时,这个来自notifyItemChange()的payload 就简单的丢掉好了。
到这里可以明白,Adapter调用这个方法可以根据指定位置Item的变化以高效地执行局部更新。接下来,在来看getChangePayload()方法:
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
官方文档是这么介绍的:
当
areItemsTheSame(int, int)
返回true并且areContentsTheSame(int, int)
方法返回false时,DiffUtil将调用此方法,获取内容变化的payload。
例如,如果将DiffUtil与RecyclerView一起使用,则可以返回Item更改的特定的字段以及RecyclerView.ItemAnimator ItemAnimator可以使用这些信息运行正确的动画。默认返回null
简单的来说,getChangePayload()方法返回的Object对象,其包括Item更改的内容,或者其他UI更新更新相关的信息,比如RecyclerView.ItemAnimator。
也就是说,当DiffUtil与RecyclerView一起使用时,如果Adapter想执行高效地局部更行,首先应重写DiffUtil.Callback的getChangePayload()方法,并将指定位置Item的变化信息返回:
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldBean = oldList[oldItemPosition]
val newBean = newList[newItemPosition]
val bundle = Bundle()
if (!TextUtils.equals(oldBean.desc, newBean.desc)) {
bundle.putString("desc", "getChangePayLoad: " + newBean.desc)
} else { // 如果没有数据变化,返回null
return null
}
return bundle
}
然后,在自定义的RecyclerView.Adapter中重写onBindViewHolder(@NonNull VH holder, int position,@NonNull List<Object> payloads)
方法,以便在Item更新内容时,Adapter是执行局部绑定还是完整绑定:
override fun onBindViewHolder(holder: DiffViewHolder, position: Int, payloads: MutableList<Any>) {
// 如果payload为null,Adapter必须运行完整绑定
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
} else {
val bundle = payloads[0] as Bundle
holder.tvDesc.text = bundle.getString(KEY_DESC)
}
}
当payloads为空时,Adapter必须执行完整绑定。至于原因看上文。
总结
- DiffUtil不仅可以配合RecyclerView,还可以与ListView等其他List配合使用,只需要自定义ListUpdateCallback即可,也就是更新操作的实现。
由于更新UI要在主线程,而DiffUtil又是耗时操作,当数据量大时,DiffUtil耗时也是漫长的,如果在主线程调用DiffUtil.calculateDiff,可能造成ANR。所以,应当开启线程执行DiffUtil.calculateDiff而在主线程调用
result.dispatchUpdatesTo(this)
分发更新操作。可以这么做:- 线程+Handler
- RxJava
对于DiffUtil不能在主线程计算差异的问题,还可以使用DiffUtil的封装类AsyncListDiffer或者ListAdapter。
3. RecyclerView.Adapter更新数据集必须放在分配更新操作之后,也就是DiffResutl调用dispatchUpdatesTo()
以后。因为在此之前更新数据集,getChangePayload()将无效,因为在getChangePayload()还需要对新旧数据集中的Item比较。