DiffUtil和它的差量算法

2023年 12月 12日 67.5k 0

DiffUtil介绍

DiffUtil是Android中的一个实用工具类,用于计算并应用RecyclerView中数据集的更改。它可以高效地计算出两个数据集之间的差异,并且只更新发生变化的部分,从而避免不必要的刷新操作,提高了RecyclerView的性能和流畅度。

DiffUtil的主要作用是在数据集发生变化时,计算出新旧数据集之间的差异,并提供给RecyclerView.Adapter进行局部刷新。它通过计算出数据集的差异,可以精确地知道哪些项目需要插入、删除、移动或更新,从而避免了全局刷新,减少了不必要的UI更新操作。

在使用DiffUtil时,需要创建一个继承自DiffUtil.Callback的类,然后在其中实现两个方法:getOldListSize()和getNewListSize()用于返回旧数据集和新数据集的大小,以及areItemsTheSame()和areContentsTheSame()用于判断两个对象是否代表同一个item和内容是否相同。DiffUtil会根据这些方法的返回结果来计算出数据集的差异,并提供给RecyclerView.Adapter进行局部刷新。

总的来说,DiffUtil差量算法能够帮助开发者高效地处理RecyclerView数据集的更新,提升了列表的性能和用户体验。

DiffUtil使用

  • 创建数据类:首先,需要创建一个数据类,用于表示RecyclerView中的每个项的数据。这个数据类需要正确地实现equals()和hashCode()方法,以便DiffUtil能够正确地比较数据项的差异。
  • 创建Adapter:接下来,创建一个RecyclerView的Adapter,并在其中实现一个继承自DiffUtil.Callback的内部类,用于计算数据集的差异。
  • 实现DiffUtil.Callback:在内部类中,需要实现DiffUtil.Callback的四个方法:
    • areItemsTheSame(oldItemPosition: Int, newItemPosition: Int):用于判断两个项是否代表同一个对象。
    • areContentsTheSame(oldItemPosition: Int, newItemPosition: Int):用于判断两个项的内容是否相同。
    • getOldListSize():返回旧数据集的大小。
    • getNewListSize():返回新数据集的大小。
  • 计算差异:在Adapter中调用DiffUtil.calculateDiff()方法,传入实现了DiffUtil.Callback的内部类,并获取DiffUtil.DiffResult对象。
  • 应用差异:最后,调用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,将计算出的差异应用到RecyclerView的Adapter上,从而更新列表项。
  • 创建一个继承自DiffUtil.ItemCallback的类,用于比较两个数据对象是否相同:

    public class MyDiffCallback extends DiffUtil.ItemCallback {
        @Override
        public boolean areItemsTheSame(@NonNull MyDataModel oldItem, @NonNull MyDataModel newItem) {
            return oldItem.getId() == newItem.getId();
        }
    
        @Override
        public boolean areContentsTheSame(@NonNull MyDataModel oldItem, @NonNull MyDataModel newItem) {
            return oldItem.equals(newItem);
        }
    }

    在你的Adapter中使用DiffUtil进行数据更新:

    public class MyAdapter extends RecyclerView.Adapter {
        private List dataList = new ArrayList();
    
        // ... 其他方法
    
        public void updateDataList(List newDataList) {
            DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(dataList, newDataList));
            dataList.clear();
            dataList.addAll(newDataList);
            diffResult.dispatchUpdatesTo(this);
        }
    
        // ... 其他方法
    }

    MyDiffCallback类用于比较两个数据对象是否相同,然后在Adapter的updateDataList方法中使用DiffUtil.calculateDiff方法计算出数据变化,并通过dispatchUpdatesTo方法应用到RecyclerView上。

    当你调用updateDataList方法更新数据时,DiffUtil会帮你计算出数据的变化,并只更新发生变化的部分,而不是整个列表都进行刷新,从而提升了性能。

    DiffUtil差量算法

    DiffUtil中的Myers算法是一种用于比较两个序列差异的算法。它通常用于RecyclerView的数据更新中,以便有效地计算出两个列表之间的差异,并且只更新发生变化的部分。

    Myers算法的核心思想是将两个序列的比较转化为一个图形化的问题,然后通过动态规划的方式来找到最小的编辑路径,从而确定两个序列之间的差异。

    在DiffUtil中,Myers算法被用于计算出两个列表之间的差异,并生成用于更新RecyclerView的操作指令,比如插入、删除、移动等操作,以便实现高效的列表更新。

    public class MyDiffUtilCallback extends DiffUtil.Callback {
    
        private List oldList;
        private List newList;
    
        public MyDiffUtilCallback(List oldList, List 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) {
            MyItem oldItem = oldList.get(oldItemPosition);
            MyItem newItem = newList.get(newItemPosition);
            return oldItem.equals(newItem);
        }
    
        @Nullable
        @Override
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            // 如果areContentsTheSame返回false,则可以在这里返回具体的变化内容,以便进行局部刷新
            return super.getChangePayload(oldItemPosition, newItemPosition);
        }
    }
  • getOldListSize()和getNewListSize()方法用于返回旧数据集和新数据集的大小。
  • areItemsTheSame()方法用于判断两个item是否代表同一个对象,通常可以根据对象的唯一标识来判断。
  • areContentsTheSame()方法用于判断两个item的内容是否相同,通常可以根据对象的内容来判断。
  • getChangePayload()方法用于返回具体的变化内容,以便进行局部刷新。
  • DiffUtil差量算法实现:

    public class DiffUtil {
        //部分代码省略
        @NonNull
        public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) {
            final int oldSize = cb.getOldListSize();
            final int newSize = cb.getNewListSize();
    
            final List snakes = new ArrayList();
    
            // instead of a recursive implementation, we keep our own stack to avoid potential stack
            // overflow exceptions
            final List stack = new ArrayList();
    
            stack.add(new Range(0, oldSize, 0, newSize));
    
            final int max = oldSize + newSize + Math.abs(oldSize - newSize);
            // allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the
            // paper for details)
            // These arrays lines keep the max reachable position for each k-line.
            final int[] forward = new int[max * 2];
            final int[] backward = new int[max * 2];
    
            // We pool the ranges to avoid allocations for each recursive call.
            final List rangePool = new ArrayList();
            while (!stack.isEmpty()) {
                final Range range = stack.remove(stack.size() - 1);
                final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
                        range.newListStart, range.newListEnd, forward, backward, max);
                if (snake != null) {
                    if (snake.size > 0) {
                        snakes.add(snake);
                    }
                    // offset the snake to convert its coordinates from the Range's area to global
                    //使路径点的偏移以将其坐标从范围区域转换为全局
                    snake.x += range.oldListStart;
                    snake.y += range.newListStart;
                    //拆分左上角和右下角进行递归
                    // add new ranges for left and right
                    final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove(
                            rangePool.size() - 1);
                    //起点为上一次的起点
                    left.oldListStart = range.oldListStart;
                    left.newListStart = range.newListStart;
                    //如果是逆向得到的中间路径,那么左上角的终点为该中间路径的起点
                    if (snake.reverse) {
                        left.oldListEnd = snake.x;
                        left.newListEnd = snake.y;
                    } else {
                        if (snake.removal) {//中间路径是向右操作,那么终点的x需要退一
                            left.oldListEnd = snake.x - 1;
                            left.newListEnd = snake.y;
                        } else {//中间路径是向下操作,那么终点的y需要退一
                            left.oldListEnd = snake.x;
                            left.newListEnd = snake.y - 1;
                        }
                    }
                    stack.add(left);
                    // re-use range for right
                    //noinspection UnnecessaryLocalVariable
                    final Range right = range;//右下角终点和之前的终点相同
                    if (snake.reverse) {
                        if (snake.removal) {//中间路径是向右操作,那么起点的x需要进一
                            right.oldListStart = snake.x + snake.size + 1;
                            right.newListStart = snake.y + snake.size;
                        } else {//中间路径是向下操作,那么起点的y需要进一
                            right.oldListStart = snake.x + snake.size;
                            right.newListStart = snake.y + snake.size + 1;
                        }
                    } else {//如果是逆向得到的中间路径,那么右下角的起点为该中间路径的终点
                        right.oldListStart = snake.x + snake.size;
                        right.newListStart = snake.y + snake.size;
                    }
                    stack.add(right);
                } else {
                    rangePool.add(range);
                }
    
            }
            // sort snakes
            Collections.sort(snakes, SNAKE_COMPARATOR);
    
            return new DiffResult(cb, snakes, forward, backward, detectMoves);
    
        }
        //diffPartial方法主要是来寻找一条snake,它的核心也就是Myers算法。
        private static Snake diffPartial(Callback cb, int startOld, int endOld,
                int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
            final int oldSize = endOld - startOld;
            final int newSize = endNew - startNew;
            if (endOld - startOld < 1 || endNew - startNew  0
                            && cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
                        x--;
                        y--;
                    }
                    backward[kOffset + backwardK] = x;
                    //如果delta为偶数,那么相连通的节点一定是反向移动的节点,也就是执行backward操作所触发的节点
                    //if delta is even and ( k >= -d - delta and k = -d && k + delta = backward[kOffset + backwardK]) {
                            Snake outSnake = new Snake();
                            outSnake.x = backward[kOffset + backwardK];
                            outSnake.y = outSnake.x - backwardK;
                            outSnake.size =
                                    forward[kOffset + backwardK] - backward[kOffset + backwardK];
                            outSnake.removal = removal;
                            outSnake.reverse = true;
                            return outSnake;
                        }
                    }
                }
            }
            throw new IllegalStateException("DiffUtil hit an unexpected case while trying to calculate"
                    + " the optimal path. Please make sure your data is not changing during the"
                    + " diff calculation.");
        }
        //部分代码省略
    }

    Myers差分算法是一种在每一步选择中都采取当前状态下最优决策的算法。在软件测试中,Myers差分算法使用了差分算法来寻找两个文本之间的最小差异集合。该算法通过比较两个文本的不同版本,找出它们之间的差异,并生成一个表示差异的最小集合。

    Myers差分算法会尽可能地选择最长的公共子序列,以便最小化差异集合的大小。这种方法在实际应用中通常能够产生较好的结果,尽管并不一定能够找到最优解。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论