使用 DiffUtil 优化 RecyclerView 更新行为

Dec 21, 2017

不要停留在使用 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 计算出差异得到 DiffResultDiffResult 再把差异分发给 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)长度。查看 官方文档 了解更多性能相关的指标。

感谢阅读,希望对你有所帮助。

/* 看板娘 */