不要停留在使用 notifyDataSetChanged() 的阶段
RecyclerView 是我们日常开发中最常用的组件之一。当我们滑动列表,我们要去更新视图,更新数据。我们会从服务器获取新的数据,需要处理旧的数据。
通常,随着每个item越来越复杂,这个处理过程所需的时间也就越多。在列表滑动过程中的处理延迟的长短,决定着对用户体验的影响的多少。所以,我们会希望需要进行的计算越少越好。
现在,我们的列表已经显示在屏幕上,获取的新的数据后需要更新,我们会调用notifyDataSetChanged() 方法。然而这个方法实际上非常消耗计算能力。因为它涉及很多迭代操作。
介于这些问题,Android 提供了一个优化类 DiffUtil 用来处理 RecyclerView
的数据更新问题。
什么是 DiffUtil
从24.2.0开始, RecyclerView 的支持库在 v7 提供了非常方便的优化类 DiffUtil。这个类帮助我们找到两个 list 的区别,然后返回更新后的 list 。这个类用来告诉 RecyclerView 的 Adapter 发生了哪些更新。
如何使用?
DiffUtil.Callback 作为 callback 类,DiffUtil 计算出差别后会回掉这个类的方法。
DiffUtil.Callback 是拥有 4 个抽象方法和 1 个非抽象方法的抽象类。我们需要继承并实现它的所有方法:
getOldListSize() 返回原始列表的 size。
getNewListSize() 返回新列表的 size。
areItemsTheSame(int oldItemPosition, int newItemPosition) 两个位置的对象是否是同一个item。
areContentsTheSame(int oldItemPosition, int newItemPosition) 决定是否两个 item 的数据是相同的。只有当 areItemsTheSame() 返回true时会调用。
getChangePayload(int oldItemPosition, int newItemPosition) 当
areItemsTheSame() 返回 true ,并且 areContentsTheSame() 返回 false 时调用,返回这个 item 更新相关的信息。
下面是一个简单的 Employee 类,使用了 EmployeeRecyclerViewAdapter 和
EmployeeDiffCallback 来完成这个列表的展示更新逻辑。
public class Employee {
public int id;
public String name;
public String role;
}
这是 DiffUtil.Callback 类的实现。注意 getChangePayload() 不是抽象方法。
public class EmployeeDiffCallback extends DiffUtil.Callback {
private final List<Employee> mOldEmployeeList;
private final List<Employee> mNewEmployeeList;
public EmployeeDiffCallback(List<Employee> oldEmployeeList, List<Employee> newEmployeeList) {
this.mOldEmployeeList = oldEmployeeList;
this.mNewEmployeeList = newEmployeeList;
}
@Override
public int getOldListSize() {
return mOldEmployeeList.size();
}
@Override
public int getNewListSize() {
return mNewEmployeeList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldEmployeeList.get(oldItemPosition).getId() == mNewEmployeeList.get(
newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
final Employee oldEmployee = mOldEmployeeList.get(oldItemPosition);
final Employee newEmployee = mNewEmployeeList.get(newItemPosition);
return oldEmployee.getName().equals(newEmployee.getName());
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
// Implement method if you're going to use ItemAnimator
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
实现了 DiffUtil.Callback,我们就可以用下面的方式更新列表了。
public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<CustomRecyclerViewAdapter.ViewHolder> {
...
public void updateEmployeeListItems(List<Employee> employees) {
final EmployeeDiffCallback diffCallback = new EmployeeDiffCallback(this.mEmployees, employees);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
this.mEmployees.clear();
this.mEmployees.addAll(employees);
diffResult.dispatchUpdatesTo(this);
}
}
调用 dispatchUpdatesTo(RecyclerView.Adapter) 方法分发更新后的列表。
DiffUtil 计算出差异得到 DiffResult ,DiffResult 再把差异分发给 Adapter,adapter 最后根据接收到的差异数据做更新。
getChangePayload() 返回的差异数据,会从 DiffResult 分发给
notifyItemRangeChanged(position, count, payload) 方法,最终交给
Adapter 的 onBindViewHolder(… List< Object > payloads) 处理。
@Override
public void onBindViewHolder(ProductViewHolder holder, int position, List<Object> payloads) {
// Handle the payload
}
DiffUtil 一般通过这四个方法通知 Adapter 来更新数据。
- notifyItemMoved()
- notifyItemRangeChanged()
- notifyItemRangeInserted()
- notifyItemRangeRemoved()
阅读 文档 可以帮助你了解更多。
重要!
如果列表很大,这个操作会花费很多时间。所以建议在后台线程计算差异,在主线程应用计算结果 DiffResult。
因为实现上的限制,list 最大尺寸限制在 2^26。
性能
DiffUtil 需要 O(N) 的空间来做差异的计算操作。预期的复杂度为
O(N+D^2),N 是需要添加和删除的 item 总数,D 是冗余(edit script)长度。查看 官方文档 了解更多性能相关的指标。
感谢阅读,希望对你有所帮助。