一、前语

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 了。

RecyclerView 之 DiffUtil

——乐于分享,共同进步,欢迎留言评论 ——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