Szhangbiao's blog

记录一些让自己可以回忆的东西

0%

DiffUtil优化RecyclerView数据加载

由于项目里大量使用了RecyclerView控件,在数据加载后进行数据刷新时只是使用了notifyDataSetChanged方法,该方法也被系统警告说效率不高,碰巧最近在做性能优化,就研究了下DiffUtil的使用,然后基于AsyncListDiffer封装了BaseDiffAdapterBaseDiffHolder,在适合场景的页面下把之前的BaseAdapterBaseHolder做了简单替换。下面就来详细介绍下DiffUtil的使用方法。

DiffUtil是官方提供的高效计算两个数据集之间差异的工具,在RecyclerView数据刷新时,可以快速的计算出需要更新的数据集,从而实现高效的数据刷新。常见的使用方法有同步计算的DiffUtil.Callback, 和异步计算的ListAdapterAsyncListDiffer,使用方式略有不同,下面分别介绍下。

DiffUtil.Callback

DiffUtil.CallbackDiffUtil的核心类,用于比较旧数据集和新数据集的元素,并计算它们之间的差异,并且可以与原有的Adapter写法解耦。它包含四个抽象方法,需要我们自行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract int getOldListSize() // 获取旧数据集的大小。

public abstract int getNewListSize() // 获取新数据集的大小。

//判断旧数据集中的某个元素和新数据集中的某个元素是否代表同一个实体。
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition)

//判断旧数据集中的某个元素和新数据集中的某个元素的内容是否相同。
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition)

//获取旧数据集中的某个元素和新数据集中的某个元素之间的差异信息。如果这两个元素相同,但是内容发生改变,可以通过这个方法获取它们之间的差异信息,从而只更新需要改变的部分,减少不必要的更新操作。
public Object getChangePayload(int oldItemPosition, int newItemPosition) // 可选

//设置是否开启移动操作的检测。如果设置为 true,DiffUtil 会检测数据集中元素的移动操作,并生成移动操作的更新列表。但是,开启移动操作的检测会增加计算量,可能会影响性能。
public boolean getMoveDetectionFlag() // 可选

创建自定义的继承DiffUtil.Callback的类,基本上我们主要关注areItemsTheSame方法和areContentsTheSame方法即可,在对比的过程中,areItemsTheSame方法返回false则会调用Adapter对应positiononCreateViewHolderareContentsTheSame方法返回false则会调用Adapter对应positiononBindViewHolder三个参数的方法,如果你的Item里的内容比较丰富可以使用getChangePayload方法来实现局部刷新。用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
YourItem oldItem = oldList.get(oldItemPosition);
YourItem newItem = newList.get(newItemPosition);

Bundle diffBundle = new Bundle();
if (!oldItem.title.equals(newItem.title)) {
diffBundle.putString("KEY_TITLE", newItem.title);
}
if (!oldItem.subtitle.equals(newItem.subtitle)) {
diffBundle.putString("KEY_SUBTITLE", newItem.subtitle);
}
if (diffBundle.size() == 0) return null;
return diffBundle;
}

在 Adapter 中处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else {
Bundle bundle = (Bundle) payloads.get(0);
if (bundle.containsKey("KEY_TITLE")) {
holder.titleTextView.setText(bundle.getString("KEY_TITLE"));
}
if (bundle.containsKey("KEY_SUBTITLE")) {
holder.subtitleTextView.setText(bundle.getString("KEY_SUBTITLE"));
}
}
}

自定义完自己的DiffUtil.Callback后,就可以把 calculate 后的DiffResult调用dispatchUpdatesTo应用到Adapter里。

1
2
3
4
5
6
List mOldList = mAdapter.getDataList();
// 计算差异
DiffUtil.Callback callback = new CustomCallback(mOldList, newList);
DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
// 通知 Adapter
result.dispatchUpdatesTo(mAdapter);

如果数据量相对较少可以使用DiffUtil.Callback这种同步计算数据差异的方式,使用起来可以跟项目里现有的Adapter封装体系解耦,数据量较大且场景合适则需要使用另外两种ListAdapterAsyncListDiffer异步计算的方式。

ListAdapter/AsyncListDiffer

ListAdapterRecyclerView.Adapter的子类,内部集成了AsyncListDiffer,比较数据差异则需要继承范型类DiffUtil.ItemCallback<T>,用法跟DiffUtil.Callback相似,但是DiffUtil.ItemCallbackareItemsTheSame方法和areContentsTheSame方法的参数类型是T类型,需要自己实现。通过submitList来提交数据集,submitList内部会自动计算数据差异,然后应用到Adapter里。

1
2
3
4
5
6
7
8
9
10
11
12
13
ListAdapter<YourItem, YourAdapter.ViewHolder> adapter = new ListAdapter<>(new DiffUtil.ItemCallback<YourItem>() {
@Override
public boolean areItemsTheSame(@NonNull YourItem oldItem, @NonNull YourItem newItem) {
return oldItem.id == newItem.id;
}

@Override
public boolean areContentsTheSame(@NonNull YourItem oldItem, @NonNull YourItem newItem) {
return oldItem.equals(newItem);
}
});
// 提交数据
adapter.submitList(newList);

这里的areItemsTheSameareContentsTheSame则需要根据具体情况来实现,需要根据不同情况来做针对性的处理。AsyncListDiffer的用法具体可以参考ListAdapter内部的实现。

具体使用ListAdapter还是AsyncListDiffer需要根据项目的封装情况来决定,拿我这次实践的情况来说,我就是基于AsyncListDiffer做了BaseDiffAdapterBaseDiffHolder的封装,只需要额外提供一个DiffUtil.ItemCallback的实现类,就可以跟原本项目里的BaseAdapterBaseHolder做替换。

DiffUtil 算法

DiffUtil基于Myers差分算法,通过以下机制实现高效的列表更新:

核心算法:

  • 使用编辑图和Snake路径计算最短编辑脚本,时间复杂度O(ND)
  • 双向搜索优化搜索效率,空间复杂度O(N)

源码实现:

  • calculateDiff()diffPartial()Myers算法,生成Snake序列。
  • computeMoves()检测移动操作,优化动画。
  • DiffResult分发更新到 Adapter,触发局部刷新。

优化手段:

  • 双向搜索、差异最小化、移动检测、空间优化和异步支持。
  • 适配大数据集、复杂动画和低内存设备。

总结

DiffUtil的使用场景是数据集发生大规模变更时高效的刷新UI,适合一次性大批量/复杂的数据变化,不适合跟单条数据增删改:notifyItemInserted/Removed/Changed一起混用,容易导致Adapter和数据源不同步,使用DiffUtil处理个别数据变动时需要先使用就数据mOldListclone 或者新建一个列表之后再对数据进行变动,不然应用到Adapter后没有效果,所以是否要使用DiffUtil取决于具体的应用场景和使用后有没有带来性能的提升。