DiffUtil和它的差量算法

DiffUtil和它的差量算法

在这里插入图片描述

前言

学习Myers'Diff 算法是从 DiffUtils 源代码开始的,但DiffUtil和它的差量算法这篇却是文章是在写完 Myers‘Diff之贪婪算法Myers‘Diff之线性空间细化 这两篇算法文章之后着手的。比较先需要学会算法才能理解代码实现并更好的进行使用。

DiffUtil介绍

在正式分析DiffUtil之前,我们先来对DiffUtil有一个大概的了解--DiffUtil到底是什么东西。

类的路径:androidx.recyclerview.widget.DiffUtil.java

大家在开发关于列表页面的时候可能会遇到下面的情况:

在一次操作里面可能会同时出现removeaddchange三种操作。像这种情况,我们不能调用notifyItemRemovednotifyItemInserted或者notifyItemChanged方法。为了视图立即刷新,我们只能通过调用notifyDataSetChanged方法来实现。但notifyDataSetChanged 刷新是全部刷新没有动画效果。

那么有一种能通过对比知道两个列表的数据的差异,然后进行removeaddchange么?

Google提供的DiffUtil是一个实用程序类,它计算两个列表之间的差异,并输出将第一个列表转换为第二个列表的更新操作列表。

DiffUtil.DiffResult

DiffUtil在计算两个列表之间的差异时使用的Callback类。

public abstract static class Callback {
    //旧数据集的长度;
    public abstract int getOldListSize();
    //新数据集的长度
    public abstract int getNewListSize();
    //判断是否是同一个item;
    public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
    //如果item相同,此方法用于判断是否同一个 Item 的内容也相同
    public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
    @Nullable
    //如果item相同,内容不同,用 payLoad 记录这个 ViewHolder 中,具体需要更新那个View
    public Object getChangePayload(int oldItemPosition, int newItemPosition){
        return null;
    }
}

DiffUtil.DiffResult

此类包含有关DiffUtil#calculateDiff调用的结果的信息。可以通过dispatchUpdatesTo使用DiffResult中的更新,也可以通过dispatchUpdatesTo直接将结果流式传输到RecyclerView.Adapter

DiffUtil使用

public class RecyclerItemCallback extends DiffUtil.Callback {
    private List<Bean> mOldDataList;
    private List<Bean> mNewDataList;
    public RecyclerItemCallback(List<Bean> oldDataList, List<Bean> newDataList) {
        this.mOldDataList = oldDataList;
        this.mNewDataList = newDataList;
    }
    @Override
    public int getOldListSize() {
        return mOldDataList.size();
    }
    @Override
    public int getNewListSize() {
        return mNewDataList.size();
    }
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return Objects.equals(mNewDataList.get(newItemPosition).getId(), mOldDataList.get(oldItemPosition).getId());
    }
    @Override
    public boolean areContentsTheSame(int i, int i1) {
        return Objects.equals(mOldDataList.get(i).getContent(), mNewDataList.get(i1).getContent());
    }
}
private void refreshData(List<Bean> oldDataList,List<Bean> newDataList) {
    RecyclerItemCallback recyclerItemCallback = new RecyclerItemCallback(oldDataList, newDataList);
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(recyclerItemCallback, false);
    diffResult.dispatchUpdatesTo(mRecyclerAdapter);
}

DiffUtil中Myers算法代码

再次提醒一下代码阅读需要先了解 Myers‘Diff之贪婪算法Myers‘Diff之线性空间细化 这两篇文章中的算法知识。

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<Snake> snakes = new ArrayList<>();

        // instead of a recursive implementation, we keep our own stack to avoid potential stack
        // overflow exceptions
        final List<Range> 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<Range> 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 < 1) {
            return null;
        }
        //差异增量
        final int delta = oldSize - newSize;
        //最双向最长路径
        final int dLimit = (oldSize + newSize + 1) / 2;
        //进行初始化设置
        Arrays.fill(forward, kOffset - dLimit - 1, kOffset + dLimit + 1, 0);
        Arrays.fill(backward, kOffset - dLimit - 1 + delta, kOffset + dLimit + 1 + delta, oldSize);
        /**
         * 差异量为奇数
         * 每个差异-水平删除或垂直插入-都是从一千行移到其相邻行。
         * 由于增量是正向和反向算法中心之间的差异,因此我们知道需要检查中间snack的d值。
         * 对于奇数增量,我们必须寻找差异为d的前向路径与差异为d-1的反向路径重叠。
         * 类似地,对于偶数增量,重叠将是当正向和反向路径具有相同数量的差异时
         */
        final boolean checkInFwd = delta % 2 != 0;
        for (int d = 0; d <= dLimit; d++) {
            /**
             * 这一循环是从(0,0)出发找到移动d步能达到的最远点
             * 引理:d和k同奇同偶,所以每次k都递增2
             */
            for (int k = -d; k <= d; k += 2) {
                // find forward path
                // we can reach k from k - 1 or k + 1. Check which one is further in the graph
                //找到前进路径
                //我们可以从k-1或k + 1到达k。检查图中的哪个更远 
                int x;
                final boolean removal;//向下
                //bool down = ( k == -d || ( k != d && V[ k - 1 ] < V[ k + 1 ] ) );
                if (k == -d || (k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1])) {
                    x = forward[kOffset + k + 1];
                    removal = false;
                } else {
                    x = forward[kOffset + k - 1] + 1;
                    removal = true;
                }
                // set y based on x
                //k = x - y
                int y = x - k;
                // move diagonal as long as items match
                //只要item匹配就移动对角线
                while (x < oldSize && y < newSize
                        && cb.areItemsTheSame(startOld + x, startNew + y)) {
                    x++;
                    y++;
                }
                forward[kOffset + k] = x;
                //如果delta为奇数,那么相连通的节点一定是向前移动的节点,也就是执行forward操作所触发的节点
                //if delta is odd and ( k >= delta - ( d - 1 ) and k <= delta + ( d - 1 ) )
                if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
                    //if overlap with reverse[ d - 1 ] on line k
                    //forward'x >= backward'x,如果在k线上正向查找能到到的位置的x坐标比反向查找达到的y坐标小
                
                    if (forward[kOffset + k] >= backward[kOffset + k]) {
                        Snake outSnake = new Snake();
                        outSnake.x = backward[kOffset + k];
                        outSnake.y = outSnake.x - k;
                        outSnake.size = forward[kOffset + k] - backward[kOffset + k];
                        outSnake.removal = removal;
                        outSnake.reverse = false;
                        return outSnake;
                    }
                }
            }
            /**
             * 这一循环是从(m,n)出发找到移动d步能达到的最远点
             */
            for (int k = -d; k <= d; k += 2) {
                // find reverse path at k + delta, in reverse
                //以k + delta,找到反向路径。backwardK相当于反向转化之后的正向的k
                final int backwardK = k + delta;
                int x;
                final boolean removal;
                //与k线类似
                //bool down = ( k == -d || ( k != d && V[ k - 1 ] < V[ k + 1 ] ) );
                if (backwardK == d + delta || (backwardK != -d + delta 
                        && backward[kOffset + backwardK - 1] < backward[kOffset + backwardK + 1])) {
                    x = backward[kOffset + backwardK - 1];
                    removal = false;
                } else {
                    x = backward[kOffset + backwardK + 1] - 1;
                    removal = true;
                }
                // set y based on x
                int y = x - backwardK;
                // move diagonal as long as items match
                //只要item匹配就移动对角线
                while (x > 0 && y > 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 - delta )
                if (!checkInFwd && k + delta >= -d && k + delta <= d) {
                    //if overlap with forward[ d ] on line k
                    //forward'x >= backward'x,判断正向反向是否连通了
                    if (forward[kOffset + backwardK] >= 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.");
    }
    //部分代码省略
}

参考链接:
代码:diff-match-patch
diff2论文
Myers diff alogrithm:part 1
Myers diff alogrithm:part 2

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容