Myers 差分算法 —— Android DiffUtils 之实现(二)

上一篇看这里:

Myers 差分算法 (Myers Difference Algorithm) —— DiffUtils
之核心算法
(一)

我们在上文简单的介绍了下 Myers 差分算法的原理,至少知道了他是怎么一回事,我们知道寻找最远的点方法有两个,一个是采用最短路径或者广度优先搜索算法,另一种是使用动态规划。

我们来看一下 Google 是怎么做的。

DiffUtil 采用的策略

首先,先不看细节,我们从入口开始看起:DiffUtil.calculateDiff,一看见一个栈

     1        final List<Range> stack = new ArrayList<>();
     2
     3        stack.add(new Range(0, oldSize, 0, newSize));
     4
     5        final int max = oldSize + newSize + Math.abs(oldSize - newSize);
     6        // allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the
     7        // paper for details)
     8        // These arrays lines keep the max reachable position for each k-line.
     9        final int[] forward = new int[max * 2];
    10        final int[] backward = new int[max * 2];
    11
    12        // We pool the ranges to avoid allocations for each recursive call.
    13        final List<Range> rangePool = new ArrayList<>();
    14        while (!stack.isEmpty()) {
    15           //...
    16        }

此处的栈实际上是把所有和Snake走出去最远的点做一个搜索算法。
这里 Google 采用了前进(Forward)和后退(Backward)两种方式来找到 Snake。

这里我们先看里面的循环 —— diffPartial

diffPartial

diffPartial主要的任务是查找 Snake,函数原型如下:

    1private static Snake diffPartial(Callback cb, int startOld, int endOld,
    2            int startNew, int endNew, int[] forward, int[] backward, int kOffset) 
    3

此处的 Callback 是业务方提供一个 Predicate Callback 供引擎使用。我们在前文的图里面,可以看见我们是使用两个坐标轴来表示老的数组和新的数组的,对应这里的oldnew,也对应了 x 值和 y 值。此处的forwardbackward记录的是从左上右下,以k为底的x值(因为 k = x + y,记录了 k 和 x,直接能得到 y)。

我们此处要记住一点:

只要 k 一致,如果 forward[k] >= backward[k],那么意味着相同的 k
值,往相反的方向走的两条步伐已经走到了一起,>
形成了一条通路,他们的轨迹已经重合,那么证明这个路径是通的,这时候,就把这个大问题分解成了两个剩余的小问题:

  1. 找到这条斜线从原点到斜线左上角中的最优解。
  2. 找到这条斜线从斜线右下角到终点的最优解。

那么解决小问题的方式就是重新递归刚刚的过程,我们这时候结合代码和图来讲。

何时返回 Snake 对象?

我们注意到,diffPartial有两个返回 Snake 对象的地方:


   1for (int d = 0; d <= dLimit; d++) {
    2    for (int k = -d; k <= d; k += 2) {
    3        //....
    4        if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
    5            if (forward[kOffset + k] >= backward[kOffset + k]) {
    6                Snake outSnake = new Snake();
    7                outSnake.x = backward[kOffset + k];
    8                outSnake.y = outSnake.x - k;
    9                outSnake.size = forward[kOffset + k] - backward[kOffset + k];
   10                outSnake.removal = removal;
   11                outSnake.reverse = false;
   12                return outSnake;
   13            }
   14        }
   15    }
   16    for (int k = -d; k <= d; k += 2) {
   17        //...
   18        // find reverse path at k + delta, in reverse
   19        if (!checkInFwd && k + delta >= -d && k + delta <= d) {
   20            if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
   21                Snake outSnake = new Snake();
   22                outSnake.x = backward[kOffset + backwardK];
   23                outSnake.y = outSnake.x - backwardK;
   24                outSnake.size =
   25                        forward[kOffset + backwardK] - backward[kOffset + backwardK];
   26                outSnake.removal = removal;
   27                outSnake.reverse = true;
   28                return outSnake;
   29            }
   30        }
   31    }
   32}

为什么会有两块地方?我们先看内部的判断条件:

forward[kOffset + k] >= backward[kOffset + k]

这里的kOffset是为了后面的k值可以取负,因为数组下标不能为负,所以 hack 了一下。
我们再回忆一下,forward 和 backward 记录的是 x 的值,下标是k,这条判断语句的意思是,在同一条斜线上,如果forward[k]的值比backward[k]大,就为 true。 其实就是从左上往右下走和从右下往左上走交汇了。我们知道,只要是斜线,都有可能交汇,但是这里是一个跟d有关的 for 循环,也就是步数最少的连通斜线。

我们还注意到一个变量checkInFwd,这个变量字面意思是,是否在前进的过程中,检查连通情况。根据的原则是老的数组长度 oldSize 和新数组长度 newSize 的差值是否为奇数,这里应该是一个均分概率的思想,我目前没有找到相关的资料,如果有详细见解的朋友欢迎一起讨论。

返回的 Snake 包含了几个要素:

  1. x 和 y
  2. Snake 的长度
  3. Snake 是否做了 x 方向上的 remove 操作
  4. Snake 是否从反向方向开始

具体可以参考 Snake 这个类里面的注释。


   1/**
    2 * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an
    3 * add or remove operation. See the Myers' paper for details.
    4 */
    5static class Snake {
    6    /**
    7     * Position in the old list
    8     */
    9    int x;
   10
   11    /**
   12     * Position in the new list
   13     */
   14    int y;
   15
   16    /**
   17     * Number of matches. Might be 0.
   18     */
   19    int size;
   20
   21    /**
   22     * If true, this is a removal from the original list followed by {@code size} matches.
   23     * If false, this is an addition from the new list followed by {@code size} matches.
   24     */
   25    boolean removal;
   26
   27    /**
   28     * If true, the addition or removal is at the end of the snake.
   29     * If false, the addition or removal is at the beginning of the snake.
   30     */
   31    boolean reverse;
   32}

Snake 的使用

Snake 返回后,我们等于找到了两个区域之内的通路,那么通路的两边就变成了两个子问题,类似如图:
在这里插入图片描述
A 和 B 是子问题

这样就只用找到从左上角到右下角连起来一块的 Snake 集合即可。
在 DiffUtils 中的就是以DiffResult这个类表示,我们在这里已经收集到了所有 Snake,Snake 中包含了所有文本修改的路径和操作,因此我们可以根据 Snake 里面的定义,对我们的 Adapter 进行一些操作。我们可以看一下 DiffResult 这个类的一些操作,一个最重要的操作是dispatchUpdatesTo,传入的参数是一个 Adapter,我们可以看下这里的操作:

     1public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
     2    dispatchUpdatesTo(new ListUpdateCallback() {
     3        @Override
     4        public void onInserted(int position, int count) {
     5            adapter.notifyItemRangeInserted(position, count);
     6        }
     7
     8        @Override
     9        public void onRemoved(int position, int count) {
    10            adapter.notifyItemRangeRemoved(position, count);
    11        }
    12
    13        @Override
    14        public void onMoved(int fromPosition, int toPosition) {
    15            adapter.notifyItemMoved(fromPosition, toPosition);
    16        }
    17
    18        @Override
    19        public void onChanged(int position, int count, Object payload) {
    20            adapter.notifyItemRangeChanged(position, count, payload);
    21        }
    22    });
    23}

这里还有个更加复杂点的操作叫DetectMoves,就是检查是不是从老的 List 上删除的数据并不是真的“删除”,而是移动到了 List 中其它的位置,我们在这里就不再赘述。

有了 DiffUtil,我们去调用notifyItemXXX系列函数就变得非常流畅,实现线性补间动画也能和 iOS 一样轻松啦(虽然也做了非常多的工作)。

如果有兴趣的同学,还可以看一下AsyncListDiffer这个类,它实现了在异步线程计算 Diff 然后在主线程通知 UI 更新的功能。里面有一些 Executors 调度器,还有一个版本控制的思路,这个思路非常值得我们在进行异步计算的学习的一种手段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值