DiffUtil使用介绍

DiffUtil是一个实用程序类,可以计算两个列表之间的差异,并输出将第一个列表转换为第二个列表的更新操作列表。
它可以用于计算RecyclerView适配器的更新。


DiffUtil官方详细介绍

DiffUtil使用Eugene W. Myers的差异算法来计算将一个列表转换为另一个列表的更新的最小数量。Myers的算法不处理移动的项目,所以DiffUtil对结果运行第二遍,以检测被移动的项目。

如果列表很大,此操作可能需要很长时间,因此建议您在后台线程上运行该命令,DiffUtil.DiffResult然后将其应用于主线程上的RecyclerView。

该算法针对空间进行了优化,并使用O(N)空间来查找两个列表之间的最小加法和删除操作数。它具有O(N + D ^ 2)预期时间性能,其中D是编辑脚本的长度。

如果启用移动检测,则需要额外的O(N ^ 2)时间,其中N是添加和删除的项目的总数。如果您的列表已经按照相同的约束进行排序(例如,创建的帖子列表的时间戳),您可以禁用移动检测以提高性能。

算法的实际运行时间显着取决于列表中的更改次数和比较方法的成本。以下是一些平均运行时间供参考:(测试列表由随机UUID字符串组成,测试运行在Nexus 5X上)

100项和10项修改:平均:0.39毫秒,中位数:0.35毫秒
100项和100修改:3.82毫秒,中位数:3.75毫秒
100项和100次无移动修改:2.09 ms,中位数:2.06 ms
1000项和50项修改:平均:4.67毫秒,中位数:4.59毫秒
1000个项目和50个修改没有移动:平均:3.59毫秒,中位数:3.50毫秒
1000项和200修改:27.07毫秒,中位数:26.92毫秒
1000项和200次修改,无移动:13.54 ms,中位数:13.36 ms
由于实施约束,列表的最大大小可以是2 ^ 26。

使用

DiffUtil核心类

  • DiffUtil.Callback:这是最核心的类,用于比较新老数据集。
  • DiffUtil:通过静态方法DiffUtil.calculateDiff(DiffUtil.Callback)来计算数据集的更新。
  • DiffResult:是DiffUtil的计算结果对象,通过DiffResult.dispatchUpdatesTo(RecyclerView.Adapter)来进行更新。

使用步骤

  • 自定义类继承DiffUtil.Callback,通过覆盖特定方法给出数据比较逻辑。
  • 调用DiffUtil.calculateDiff(DiffUtil.Callback callback[, boolean detectMove])来计算更新,得到DiffResult对象。第二个参数可省,该参数仅用于是否探测数据的移动,是否关闭需要根据数据集情况来权衡。
    如果您的旧的和新的列表按照相同的约束排序,并且项目永远不会移动(交换位置),则可以禁用移动检测,这需要O(N^2)时间,其中N是添加,移动和删除的项目的数量。
    当数据集很大时,此操作可能耗时较长,需要异步计算。
  • 在UI线程中调用DiffResult.dispatchUpdatesTo([RecyclerView.Adapter adapter] || [ListUpdateCallback updateCallback]),而后Adapter的onBindViewHolder(RecyclerView.ViewHolder holder, int position, Listpayloads)。注意这个方法比必须覆盖的onBindViewHolder(RecyclerView.ViewHolder holder, int position)方法多一个参数payloads,而里面存储了数据的更新。
    当dispatchUpdatesTo参数为RecyclerView.Adapter在ListUpdateCallback几个回调中分别调用以下几个方法:
    • adapter.notifyItemRangeInserted(position, count);
    • adapter.notifyItemRangeRemoved(position, count);
    • adapter.notifyItemMoved(fromPosition, toPosition);
    • adapter.notifyItemRangeChanged(position, count, payload);

核心示例代码

继承DiffUtil.Callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MainDiffCallback extends DiffUtil.Callback {

private List<Item> oldList;
private List<Item> newList;

public MainDiffCallback(List<Item> oldList, List<Item> newList) {
this.oldList = oldList;
this.newList = newList;
}

@Override
public int getOldListSize() {
return oldList.size();
}

@Override
public int getNewListSize() {
return newList.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}

@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
Item oldItem = oldList.get(oldItemPosition);
Item newItem = newList.get(newItemPosition);
Bundle diffBundle = new Bundle();
if (!newItem.getTitle().equals(oldItem.getTitle())) {
diffBundle.putString(Item.KEY_TITLE, newItem.getTitle());
}
if (!newItem.getContent().equals(oldItem.getContent())) {
diffBundle.putString(Item.KEY_CONTENT, newItem.getContent());
}
if (!newItem.getFooter().equals(oldItem.getFooter())) {
diffBundle.putString(Item.KEY_FOOTER, newItem.getFooter());
}
return diffBundle;
}
}

使用DiffUtil新旧数据处理

1
2
3
4
// 这里主要数据比较小
// 数据稍微比较多,需要异步执行
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MainDiffCallback(oldData, newData));
diffResult.dispatchUpdatesTo(mMainAdapter);

获取更新数据,并更新UI

  • 重写RecylerView.Adapter.onBindViewHolder(RecyclerView.ViewHolder holder, int position, Listpayloads)方法
  • 通过payloads.get(0)获取到在DiffUtil.Callback.getChangePayload()方法中返回的Bundle,并取出数据更新情况以更新UI。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 关键辅助核心代码
    if(payloads!=null && payloads.size()>0) {
    Bundle o = (Bundle) payloads.get(0);
    Item item = new Item();
    for (String key : o.keySet()) {
    switch (key) {
    case Item.KEY_TITLE:
    item.setTitle(o.getString(Item.KEY_TITLE));
    break;
    case Item.KEY_CONTENT:
    item.setContent(o.getString(Item.KEY_CONTENT));
    break;
    case Item.KEY_FOOTER:
    item.setFooter(o.getString(Item.KEY_FOOTER));
    break;
    }
    }
    }

总结

  • DiffUtil可以用于高效计算RecyclerView适配器的更新。
  • 但DiffUtil主要作用是计算数据集最小更新,同时DiffUtil有强大的算法支撑。
  • 故,我们还可以利用DiffUtil完成许多其他类似数据更新的其他功能。