目录

RecyclerView性能提升神器-DiffUtil(前序)


  • DiffUtil这个类早前既有知晓,一直没有用它,今天刚好产品上新的需求要求刷新上的体验及性能都比较苛刻,故而想起了DiffUtil。
  • 趁这个机会好好了解下DiffUtil这个类,接下来我们先从Google的官方介绍来看下DiffUtil这个类都有什么作用

原文如下:

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.

It can be used to calculate updates for a RecyclerView Adapter. See ListAdapter and AsyncListDiffer which can compute diffs using DiffUtil on a background thread.

DiffUtil uses Eugene W. Myers’s difference algorithm to calculate the minimal number of updates to convert one list into another. Myers’s algorithm does not handle items that are moved so DiffUtil runs a second pass on the result to detect items that were moved.

If the lists are large, this operation may take significant time so you are advised to run this on a background thread, get the DiffUtil.DiffResult then apply it on the RecyclerView on the main thread.

This algorithm is optimized for space and uses O(N) space to find the minimal number of addition and removal operations between the two lists. It has O(N + D^2) expected time performance where D is the length of the edit script.

If move detection is enabled, it takes an additional O(N^2) time where N is the total number of added and removed items. If your lists are already sorted by the same constraint (e.g. a created timestamp for a list of posts), you can disable move detection to improve performance.

The actual runtime of the algorithm significantly depends on the number of changes in the list and the cost of your comparison methods. Below are some average run times for reference: (The test list is composed of random UUID Strings and the tests are run on Nexus 5X with M)

100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms 100 items and 100 modifications: 3.82 ms, median: 3.75 ms 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms Due to implementation constraints, the max size of the list can be 2^26.

大体意思是:

  • DiffUtil是一个实用型的工具类,他可以计算两个两个集合的不同并输入一个更新操作的集合,该集合包含了从第一个集合转换到第二个集合的更新操作

  • 它也可以在RecyclverView的Adapter去实用提供计算更新操作。可以看ListAdapter和AsyncListDiffer这两个类,其在后台线程中被使用去计算差异。

  • DiffUtil使用Eugene W. Myers的差分算法来计算将一个列表转换为另一个列表的最小更新次数。Myers的算法不处理移动的项目,因此DiffUtil在结果上运行第二次传递以检测已移动的项目。

  • 如果列表很大,此操作可能需要很长时间,因此你应该在后台线程上运行此操作,然后从DiffUtil.DiffResult获取计算结果并将其应用于主线程上的RecyclerView。

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

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

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

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


  • 虽然后面Google官方提供了 ListAdapter 和 AsyncListDiffer这连个类,不过其在version27之后才引入了,所以在老项目中使用是不显示的,但是DiffUtil是在v7包中的。

DiffUtil的优点:

  • 在没有DiffUtil之前,一般我们都是无脑mAdapter.notifyDataSetChanged()。
  • 调用notifyDataSetChanged()不好的地方如下:
    • 1、不会触发RecyclverView的动画,如果设计上需要有相关的动画就会比较蛋疼。
    • 2、技术部分数据并没有发生变化,也会无脑的刷新,浪费cpu资源,极差的情况下效率极低。

在使用DiffUtil之前我们还需要了解其内部的三个内部类:

DiffUtil.Callback
  • DiffUtil.Callback:在DiffUtil计算两个队列的差异时回通过该类进行回调。
返回值 方法名 描述
abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition) Called by the DiffUtil when it wants to check whether two items have the same data.在DiffUtil想去检测两个Items是否有一样的数据时调用,,True表示一致,False表示不一致
abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition) Called by the DiffUtil to decide whether two object represent the same Item.在DiffUtil检测两个对象是否表示相同的Item时被调用,True代表两个对象对应相同的Item
Object getChangePayload(int oldItemPosition, int newItemPosition) When areItemsTheSame(int, int) returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change.在areItemsTheSame返回True时,areContentsTheSame返回false时,也就是一个Item的内容发生了变化,而这个变化有可能是局部的(例如微博的点赞,我们只需要刷新图标而不是整个Item)。所以可以在getChangePayload()中封装一个Object来告诉RecyclerView进行局部的刷新。使用该功能时我们需要重写Adapter的public void onBindViewHolder(DiffVH holder, int position, List payloads) 回调,payloads及为我们返回的Object,我们可以通过bundle进行数据的传递
abstract int getNewListSize() Returns the size of the new list.返回新数据列表的大小
abstract int getOldListSize() Returns the size of the old list.返回老数据列表的大小

DiffUtil.DiffRes
  • 此类包含有关calculateDiff(Callback,boolean)调用结果的信息。 我们可以通过dispatchUpdatesTo(ListUpdateCallback)在DiffResult中使用更新,也可以通过dispatchUpdatesTo(RecyclerView.Adapter)将结果直接传输到RecyclerView.Adapter。
  • 该类主要有两个方法
返回值 方法名 描述
void dispatchUpdatesTo(ListUpdateCallback updateCallback) Dispatches update operations to the given Callback. 分发更新操作给回调,这些更新是原子的,因此第一次更新调用会影响它之后的每个更新调用(与RecyclerView相同)。
void dispatchUpdatesTo(Adapter adapter) Dispatches the update events to the given adapter. 分发更新时间给adapte,例如,如果您有一个由List支持的适配器,您可以将该列表与新列表交换,然后调用此方法将所有更新分派给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);
                    }
                });
            }
    
  • 调用示例代码如下:

    List oldList = mAdapter.getData();
         DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
         mAdapter.setData(newList);
         result.dispatchUpdatesTo(mAdapter);
    
  • 请注意,RecyclerView要求您在更改数据时立即分派适配器更新(不能推迟通知*调用)。 上面的用法遵循此规则,因为在更改后备数据之后,在RecyclerView尝试读取之前,会立即将更新发送到适配器。


DiffUtil.ItemCallback
  • 用于计算列表中两个非空项之间的差异的回调。
  • DiffUtil.Callback有两个角色 - 列表索引和项目差异。 ItemCallback只处理其中的第二个,它允许从表示层和内容特定的差异代码中分离索引到数组或List的代码。
返回值 方法名 描述
abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition) Called by the DiffUtil when it wants to check whether two items have the same data.在DiffUtil想去检测两个Items是否有一样的数据时调用,,True表示一致,False表示不一致
abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition) Called by the DiffUtil to decide whether two object represent the same Item.在DiffUtil检测两个对象是否表示相同的Item时被调用,True代表两个对象对应相同的Item
Object getChangePayload(int oldItemPosition, int newItemPosition) When areItemsTheSame(int, int) returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change.在areItemsTheSame返回True时,areContentsTheSame返回false时,也就是一个Item的内容发生了变化,而这个变化有可能是局部的(例如微博的点赞,我们只需要刷新图标而不是整个Item)。所以可以在getChangePayload()中封装一个Object来告诉RecyclerView进行局部的刷新。使用该功能时我们需要重写Adapter的public void onBindViewHolder(DiffVH holder, int position, List payloads) 回调,payloads及为我们返回的Object,我们可以通过bundle进行数据的传递
  • 也就是与DiffUtil.Callback两者根据实际情况使用其中一个即可。
  • 基本使用我们放在下一篇