一、前言
DiffUtils 是 Support-v7:24:2.0 中,更新的工具类,主要是为了配合RecyclerView 使用,通过比对新、旧两个数据集的差异,生成旧数据到新数据的最小变动,然后对有变动的数据项,进行局部刷新。
DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.
官方文档:
https://developer.android.com/reference/android/support/v7/util/DiffUtil
参考链接:
https://blog.csdn.net/zxt0601/article/details/52562770
https://www.jianshu.com/p/b9af71778b0d
https://medium.com/@iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00
https://medium.com/mindorks/diffutils-improving-performance-of-recyclerview-102b254a9e4a
https://segmentfault.com/a/1190000007205469
https://juejin.im/entry/57bbb7f60a2b58006cbd9e0c
https://proandroiddev.com/diffutil-is-a-must-797502bc1149
二、为什么会推出DiffUtil
RecyclerView是我们日常开发中最常用的组件之一。当我们滑动列表,我们要去更新视图,更新数据。我们会从服务器获取新的数据,需要处理旧的数据。通常,随着每个item越来越复杂,这个处理过程所需的时间也就越多。在列表滑动过程中的处理延迟的长短,决定着对用户体验的影响的多少。所以,我们会希望需要进行的计算越少越好。
RecyclerView 自从被发布以来,一直被说成是 ListView、GridView 等一系列列表控件的完美替代品。并且它本身使用起来也非常的好用,布局切换方便、自带ViewHolder、局部更新并且可带更新动画等等。
局部更新、并且可以很方便的设置更新动画这一点,是 RecyclerView 一个不错的亮点。它为此提供了对应的方法:
- adapter.notifyItemChange()
- adapter.notifyItemInserted()
- adapter.notifyItemRemoved()
- adapter.notifyItemMoved()
以上方法都是为了对数据集中,单一项进行操作,并且为了操作连续的数据集的变动,还提供了对应的 notifyRangeXxx() 方法。虽然 RecyclerView 提供的局部更新的方法,看似非常的好用,但是实际上,其实并没有什么用。在实际开发中,最方便的做法就是无脑调用 notifyDataSetChanged(),用于更新 adapter 的数据集。
虽然 notifyDataSetChanged 有一些缺点:
- 不会触发 RecyclerView 的局部更新的动画。
- 性能低,会刷新整个 RecyclerView 可视区域((all visible view on screen and few buffer view above and below the screen))
但是真有需要频繁刷新,前后有两个数据集的场景,一个 notifyDataSetChanged() 方法,会比自己写一个数据集比对方法,然后去计算他们的差值,最后调用对应的方法更新到 RecyclerView 中去要更方便。于是,Google就发布了DiffUtil。
有一个特别适合使用的场景便是下拉刷新
,不仅有动画,效率也有提高,尤其是下拉刷新
操作后,Adapter
内集合数据并没有发生改变,不需要进行重新绘制RecyclerView
时。
三、介绍DiffUtil
它能很方便的对两个数据集之间进行比对,然后计算出变动情况,配合RecyclerView.Adapter ,可以自动根据变动情况,调用 adapter 的对应方法。当然,DiffUtil 不仅只能配合 RecyclerView 使用,它实际上可以单独用于比对两个数据集,然后如何操作是可以定制的,那么在什么场景下使用,就全凭我们自己发挥了。
DiffUtil 在使用起来,主要需要关注几个类:
- DiffUtil.Callback:具体用于限定数据集比对规则。
- DiffUtil.DiffResult:比对数据集之后,返回的差异结果。
1、DiffUtil.Callback
DiffUtil.Callback 主要就是为了限定两个数据集中子项的比对规则。毕竟开发者面对的数据结构多种多样,既然没法做一套通用的内容比对方式,那么就将比对的规则,交还给开发者来实现即可。
它拥有 4 个抽象方法和 1 个非抽象方法的抽象类。我们需要继承并实现它的所有方法:在自定义的 Callback 中,其实需要实现 4 个方法:
- getOldListSize():旧数据集的长度。
- getNewListSize():新数据集的长度
- areItemsTheSame():判断是否是同一个Item。
- areContentsTheSame():如果是通一个Item(即areItemsTheSame返回true),此方法用于判断是否同一个 Item 的内容也相同。
前两个是获取数据集长度的方法,这没什么好说的。但是后两个方法,主要是为了对应多布局的情况产生的,也就是存在多个 viewType 和多个 ViewHodler 的情况。首先需要使用 areItemsTheSame() 方法比对是否是同一个 viewType(也就是同一个ViewHolder) ,然后再通过 areContentsTheSame() 方法比对其内容是否也相等。
其实 Callback 还有一个 getChangePayload() 的方法,它可以在 ViewType 相同,但是内容不相同的时候,用 payLoad 记录需要在这个 ViewHolder 中,具体需要更新的View。
areItemsTheSame()、areContentsTheSame()、getChangePayload() 分别代表了不同量级的刷新。
首先会通过 areItemsTheSame() 判断当前 position 下,ViewType是否一致,如果不一致就表明当前position下,从数据到UI结构上全部变化了,那么就不关心内容,直接更新就好了。如果一致的话,那么其实View是可以复用的,就还需要再通过 areContentsTheSame() 方法判断其内容是否一致,如果一致,则表示是同一条数据,不需要做额外的操作。但是一旦不一致,则还会调用 getChangePayload() 来标记到底是哪个地方的不一样,最终标记需要更新的地方,最终返回给 DiffResult 。
当然,对性能要是要求没那么高的情况下,是可以不使用 getChangedPayload() 方法的。
2、DiffUtil.DiffResult
DiffUtil.DiffResult 其实就是 DiffUtil 通过 DiffUtil.Callback 计算出来,两个数据集的差异。它是可以直接使用在 RecyclerView 上的。
3、使用DiffUtil
介绍了 Callback 和 DiffResult 之后,其实就可以正常使用 DiffUtil 来进行数据集的比对了。
在这个过程中,其实其实很简单,只需要调用两个方法:
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mOldList, mList), true);
diffResult.dispatchUpdatesTo(myAdapter);
calculateDiff 方法主要是用于通过一个具体的 DiffUtils.Callback 实现对象,来计算出两个数据集差异的结果,得到 DiffUtil.DiffResult 。而 calculateDiff 的另外一个参数,用于标记是否需要检测 Item 的移动,
而 dispatchUpdatesTo() 就是将这个数据集差异的结果,通过 adapter 更新到 RecyclerView 上面,自动调用以下四个方法:
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
adapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
adapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
adapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
adapter.notifyItemRangeChanged(position, count, payload);
}
});
}
DiffUtil 使用的是 Eugene Myers 的Difference 差别算法,这个算法本身是不检查元素的移动的。也就是说,有元素的移动它也只是会先标记为删除,然后再标记插入(即 calculateDiff 的第三个参数为 false 时)。而如果需要计算元素的移动,它实际上也是在通过 Eugene Myers 算法比对之后,再进行一次移动检查。所以,如果集合本身已经排序过了,可以不进行移动的检查。
而如果添加了对数据条目移动的识别,复杂度就会提高到O(N^2)。所以如果数据集中数据不存在移位情况,你可以关闭移动识别功能来提高性能。
四、使用
1、自定义继承自 DiffUtil.Callback 的类
RecyclerView 中使用单一 ViewType ,并且使用一个 TextView 承载一个 字符串来显示。
package com.example.zhangruirui.coordinatorlayoutdemo;
import android.support.v7.util.DiffUtil;
import java.util.List;
public class DiffCallBack extends DiffUtil.Callback {
private List<String> mOldDatas, mNewDatas;
public DiffCallBack(List<String> oldDatas, List<String> newDatas) {
this.mOldDatas = oldDatas;
this.mNewDatas = newDatas;
}
// 老数据集 size
@Override
public int getOldListSize() {
return mOldDatas != null ? mOldDatas.size() : 0;
}
// 新数据集 size
@Override
public int getNewListSize() {
return mNewDatas != null ? mNewDatas.size() : 0;
}
/**
* Called by the DiffUtil to decide whether two object represent the same Item.
* 被 DiffUtil 调用,用来判断两个对象是否是相同的 Item。
* For example, if your items have unique ids, this method should check their id equality.
* 例如,如果你的Item有唯一的id字段,这个方法就判断id是否相等。
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list
* @return True if the two items represent the same object or false if they are different.
*/
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
// Log.e("zhangrr", "areItemsTheSame: " + (oldItemPosition == newItemPosition)
// + " oldItemPosition = " + oldItemPosition + " newItemPosition = " + newItemPosition);
// return oldItemPosition == newItemPosition;
return mOldDatas.get(oldItemPosition).equals(mNewDatas.get(newItemPosition));
// return mOldDatas.get(oldItemPosition).getClass().equals(mNewDatas.get(newItemPosition).getClass());
}
/**
* Called by the DiffUtil when it wants to check whether two items have the same data.
* 被 DiffUtil 调用,用来检查两个 item 是否含有相同的数据
* DiffUtil uses this information to detect if the contents of an item has changed.
* DiffUtil 用返回的信息(true false)来检测当前 item 的内容是否发生了变化
* DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
* DiffUtil 用这个方法替代 equals 方法去检查是否相等。
* so that you can change its behavior depending on your UI.
* 所以你可以根据你的 UI 去改变它的返回值
* For example, if you are using DiffUtil with a
* {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
* return whether the items' visual representations are the same.
* 例如,如果你用 RecyclerView.Adapter 配合 DiffUtil 使用,你需要返回 Item 的视觉表现是否相同。
* This method is called only if {@link #areItemsTheSame(int, int)} returns
* {@code true} for these items.
* 这个方法仅仅在 areItemsTheSame() 返回 true 时,才会被调用。
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list which replaces the
* oldItem
* @return True if the contents of the items are the same or false if they are different.
*/
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
String oldData = mOldDatas.get(oldItemPosition);
String newData = mNewDatas.get(newItemPosition);
// Log.e("zhangrr", "areContentsTheSame: " + oldData.equals(newData)
// + " oldItemPosition = " + oldItemPosition + " newItemPosition = " + newItemPosition);
return oldData.equals(newData);
}
/**
* When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
* {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
* calls this method to get a payload about the change.
* 定向刷新中的局部更新
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list which replaces the
* oldItem
* @return A payload object that represents the change between the two items.
*/
// @Nullable
// @Override
// public Object getChangePayload(int oldItemPosition, int newItemPosition) {
// String oldData = mOldDatas.get(oldItemPosition);
// String newData = mNewDatas.get(newItemPosition);
//
// Bundle payload = new Bundle();
// if (oldData != newData){
// payload.putString("NEW_DATA", newData);
// }
// Log.e("zhangrr", "getChangePayload() called with: oldItemPosition = [" + oldItemPosition + "], newItemPosition = "
// + newItemPosition + " oldData = [" + oldData + "], newData = [" + newData + " payload = " + payload.size());
// return payload.size() == 0 ? null : payload;
// }
}
2、更新数据集
此处通过单击item时模拟数据更新操作
myAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
@Override
public void onClick(int position) {
Toast.makeText(getActivity(), "您选择了 " + mList.get(position),
Toast.LENGTH_SHORT).show();
mList.set(position, "new " + " item");
final long startTime = SystemClock.uptimeMillis();
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mOldList, mList), true);
Log.e("zhangrr", "onLongClick() called with: dialog = [" + mOldList.size() + "], i = [" + mList.size() + "]"
+ " 计算时延 = " + (SystemClock.uptimeMillis() - startTime));
diffResult.dispatchUpdatesTo(myAdapter);
mOldList = new ArrayList<>(mList);
myAdapter.setDatas(mList);
}
3、DiffUtil 的效率问题
通过测试不同量级的数据集,可发现
private void initData(String titleText) {
mList = new ArrayList<>(100000);
// 不新开线程,数据量 1000 的时候,耗时 7ms
// 不新开线程,数据量 10000 的时候,耗时 29ms
// 不新开线程,数据量 100000 的时候,耗时 105ms
// 所以我们应该将获取 DiffResult 的过程放到子线程中,并在主线程中更新 RecyclerView
// 此处使用 RxJava,当数据量为 100000 的时候,耗时 13ms
for (int i = 0; i < 100000; i++) {
mList.add(titleText + " 第 " + i + " 个item");
}
mOldList = new ArrayList<>(mList);
}
所以当数据集较大时,你应该在后台线程计算数据集的更新。官网也考虑到这点,于是发布了 AsyncListDiffer 用于在后台执行计算差异的逻辑。
虽然后面 Google 官方提供了 ListAdapter 和 AsyncListDiffer这连个类,不过其在 version27 之后才引入了,所以在老项目中使用是不显示的,但是 DiffUtil 是在v7包中的。
此处使用 RxJava 对前面的逻辑进行修改
private void doCalculate() {
Observable.create(new ObservableOnSubscribe<DiffUtil.DiffResult>() {
@Override
public void subscribe(ObservableEmitter<DiffUtil.DiffResult> e) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mOldList, mList), false);
e.onNext(diffResult);
}
}).subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<DiffUtil.DiffResult>() {
@Override
public void accept(DiffUtil.DiffResult diffResult) {
diffResult.dispatchUpdatesTo(myAdapter);
mOldList = new ArrayList<>(mList);
myAdapter.setDatas(mList);
}
});
}
在监听事件中进行方法调用
myAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
@Override
public void onClick(int position) {
Toast.makeText(getActivity(), "您选择了 " + mList.get(position),
Toast.LENGTH_SHORT).show();
mList.set(position, "new " + " item");
final long startTime = SystemClock.uptimeMillis();
doCalculate();
Log.e("zhangrr", "onLongClick() called with: dialog = [" + mOldList.size() + "], i = [" + mList.size() + "]"
+ " 计算时延 = " + (SystemClock.uptimeMillis() - startTime));
}
五、备注(待商榷)
发现之前一个错误的写法,在 dispatchUpdatesTo(adapter) 之后才应该使用 adapter.setDatas 更新 adapter 里面的数据集,因为 Callback 的 getChangePayload 方法是在 dispatchUpdatesTo 之后执行,如果先 adapter.setDatas 更新了数据,那么 adapter 内的数据集和新的数据集内容就是一样了。这样 getChangePayload 就返回 null 了。
image.png
网友评论