一、前语
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.
官方文档:
developer.android.com/reference/a…
参阅链接:
blog.csdn.net/zxt0601/art…
www.jianshu.com/p/b9af71778…
medium.com/@iammert/us…
medium.com/mindorks/di…
segmentfault.com/a/119000000…
juejin.im/entry/57bbb…
proandroiddev.com/diffutil-is…
二、为什么会推出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 了。
——乐于分享,共同进步,欢迎留言评论 ——Treat Warnings As Errors ——Any comments greatly appreciated ——Talking is cheap, show me the code ——CSDN:blog.csdn.net/u011489043 ——简书:www.jianshu.com/u/4968682d5… ——GitHub:github.com/selfconzrr