RecyclerView局部刷新和Diff算法

概述

RecyclerView中有许多神奇的特性,好比局部刷新,它不只能够针对某个item进行刷新,也能够针对item中的某些数据进行刷新。这对咱们页面的页面渲染带来了很大的提高。那么RecyclerView是怎么经过对新旧数据的对比来作到局部刷新的?更进一步,对比新旧数据的这个Diff算法又是什么的样子的。下面将会从这两个部分来展开讨论。git

RecyclerView的局部刷新

局部刷新api

RecyclerView为了应对局部刷新提供了下面几个方法:github

  • mAdapter.notifyItemChanged(position)
  • mAdapter.notifyItemInserted(position)
  • mAdapter.notifyItemRemoved(position)
  • mAdapter.notifyItemMoved(fromPosition, toPosition)

为了对数据集的改动还提供了notifyRange**系列的方法。可是这些方法在DiffUtil类出现以前其实使用的场景颇有限。大部分状况都是直接的调用notifyDataSetChanged()方法。可是这个方法有一些明显的缺点:算法

  • 不会触发Item动画
  • 对整个可见区域进行刷新

DiffUtil实现局部刷新

上面说到其实RecyclerView是有提供局部刷新的方法的,可是这些方法不多被用到,缘由是咱们没有一个高效处理新旧数据差别的方法,直到DiffUtil类的出现。使用DiffUtil的话只须要关注两个类:数据库

  • DiffUtil.Callback

咱们须要经过这个类实现咱们的diff规则。它有四个方法:api

  • getOldListSize(int position):旧数据集的长度。
  • getNewListSize(int position):新数据集的长度。
  • areItemsTheSame(int position):判断是不是同一个Item。
  • areContentsTheSame(int position):若是是通一个Item,判断Item 的内容是否相同。

下面简单的介绍一下方法3和方法4,areItemsTheSame判断是不是同一个item,通常使用item数据中的惟一id来判断,例如数据库中的id。areContentsTheSame 这个方法是在方法3返回true的时候才会被调用。要解决的问题是item中某些字段内容的刷新。例如朋友圈中更新点赞的状态。实际上是不须要对整个item进行刷新的,只须要点赞所对应的控件进行刷新。bash

  • DiffUtil.DiffResult DiffUtil.DiffResult 保存了经过 DiffUtil.Callback 计算出来,两个数据集的差别。它是能够直接使用在 RecyclerView 上的。若是有必要,也是能够经过实现 ListUpdateCallback 接口,来比对这些差别。

上面这些内容会有个demo帮助理解工具

Diff算法

在上面使用RecyclerView作局部刷新的时候,使用了一个DiffUtil工具类。那么这个工具类是基于什么样的算法实现的? 要讲明白这个问题需先介绍一些概念post

概念解释

  • 什么是diff diff是两个数据之间的区别(这里的数据能够是文件,字符串等等),也就是将源数据变成目标数据所须要的操做。咱们研究diff算法就是找到一种操做最少的方式。动画

  • Myers Diff算法ui

DiffUtil这个工具类使用的Diff算法来自于Eugene W. Myers在1986年发表的一篇算法论文

  • 有向编辑图(Edit graph)

算法依赖于新旧数据(定义为A和B构成的有向编辑图, 图中A为X轴, B为Y轴, 假定A和B的长度分别为M, N, 每一个坐标表明了各自字符串中的一个字符. 在图中沿X轴前进表明删除A中的字符, 沿Y轴前进表明插入B中的字符. 在横坐标于纵坐标字符相同的地方, 会有一条对角线链接左上与右下两点, 表示不需任何编辑, 等价于路径长度为0. 算法的目标, 就是寻找到一个从坐标(0, 0)到(M, N)的最短路径

  • Trace

    路径中斜线边的“匹配点”组成的序列,长度为L

  • 最短编辑脚本(SES Shortest Edit Script)

    仅包含两种操做:删除和添加。从(0, 0)到(M, N)删除了N-L个字符,添加了M-L个字符,对于每个trace,有一个对应的编辑脚本D = M + N - 2L

  • 最长公共子序列(LCS Longest Common Subsequence)

    LCS是两个字符串中去掉一些字符后,所产生的共有的最长的字符串序列,注意,这与最长公共字符串是不一样的,后者是必须连续的。寻找LCS其实就是寻找Trace的最大长度。

寻找LCS的问题与寻找一条从(0, 0)到(M, N)同时有最多数量的斜边是等价的

寻找SES与寻找一条从(0, 0)到(M, N)同时有最少数量的非斜边的问题是等价的

  • snake(蛇形线)

一条snake表示横(竖)方向移动一步后接着尽量多的斜边方向的移动,组成的线

  • 斜线k

k = x - y定义的一条直线,也就是k相同的点组成的一条直线。k线之间是相互平行的斜线。

  • D-path

移动D步后的点的连线,D的大小是不包含斜线的数量的

两个引理

  • 引理1 一个D-path的终点必定在斜线k上, 其中 k ∈ { -D, -D + 2, ... D -2 , D} 。证实可使用数学概括法,详细证实见论文

  • 引理2

0-path的最远到达点为(x, x),其中x ∈ min(z - 1 || az ≠ bz or z > M 或 z > N)。D-path的最远到达点在k线上,能够被分解为在k-1 线上的(D-1)-path,跟着一条横向边,接着一条越长越好的斜边 和 在k+1 线上的(D-1)-path,跟着一条竖向边,接着一条越长越好的斜边 详细证实见论文

这条引理是定义Snake的理论依据,另外此引理包含了一条贪婪原则: D-path能够经过贪婪地延伸(D-1)-path的最远到达点得到。 这里可能仍是不大明白,下面会详细的介绍这个过程。

算法过程

将上面那张图作个变换,获得下面的图:

咱们从坐标(0, 0)开始,此时,d=0,k=0,而后逐步增长d,计算每一个k值下对应的最优坐标。

由于每一步要么向右(x + 1),要么向下(y + 1),要么就是对角线(x和y都+1),因此,当d=1时,k只可能有两个取值,要么是1,要么是-1。

  • 当d=1

    k=1时,最优坐标是(1, 0)。

    k=-1时,最优坐标是(0, 1)。

    由于d=1时,k要么是1,要么是-1,当d=2时,表示在d=1的基础上再走一步,k只有三个可能的取值,分别是-2,0,2。

  • 当d=2

    k=-2时,最优坐标是(2, 4)。 k=0时,最优坐标是(2, 2)。 k=2时,最优坐标是(3, 1)。 由于d=2时, k的取值是-2, 0, 2,因此当d=3时,表示只能在d=2的基础上再走一步。k的取值为 -3, -1, 1, 3

  • 当d=3

    k = -3时, 只能从k=-2向下移动,即(2, 4)向下移动至(2, 5)经斜线至(3, 6),因此最优坐标是(3, 6)

    k = -1时,​能够由k=-2向右移动,即(2, 4)向右移动至(3, 4)经斜线至(4, 5) ​也可由k=0向下移动,即(2, 2)向下移动至(2, 3) ​由于一样在k = -1线上,(4, 5)比(2, 3)更远,因此最优坐标是(4, 5)

    k = 1时, 能够由k = 0向右移动,即(2, 2)向右移动至(3, 2)经斜线至(5, 4), 也可由k = 2向下移动,即(3, 1)向下移动至(3, 2)经斜线 至(5, 4),因此最优坐标是(5, 4)

    k = 3时, 只能从 k = 2 向右移动,即(3, 1)向右移动至(4, 1)经斜线至(5, 2),因此最优坐标是(5, 2)

以此类推,直到咱们找到一个d和k值,达到最终的目标坐标(7, 6)。

这个问题的解决用到了一个贪婪算法的原则:

D-path能够经过贪婪地延伸(D-1)-path的最远到达点得到

通俗的理解就是一个问题的解决,依赖其子问题的最优解。

代码实现

public class DiffSample {

    public static void main(String[] args) {

        String a = "ABCABBA";
        String b = "CBABAC";

        char[] aa = a.toCharArray();
        char[] bb = b.toCharArray();

        int max = aa.length + bb.length;

        int[] v = new int[max * 2];
        List<Snake> snakes = new ArrayList<>();

        for (int d = 0; d <= aa.length + bb.length; d++) {
            System.out.println("D:" + d);
            for (int k = -d; k <= d; k += 2) {
                System.out.print("k:" + k);
                // 向下 or 向右?
                boolean down = (k == -d || (k != d && v[k - 1 + max] < v[k + 1 + max]));
                int kPrev = down ? k + 1 : k - 1;

                // 开始坐标
                int xStart = v[kPrev + max];
                int yStart = xStart - kPrev;

                // 中间坐标
                int xMid = down ? xStart : xStart + 1;
                int yMid = xMid - k;

                // 终止坐标
                int xEnd = xMid;
                int yEnd = yMid;

                int snake = 0;
                while (xEnd < aa.length && yEnd < bb.length && aa[xEnd] == bb[yEnd]) {
                    xEnd++;
                    yEnd++;
                    snake++;
                }
                // 保存最终点
                v[k + max] = xEnd;
                // 记录 snake
                snakes.add(0, new Snake(xStart, yStart, xEnd, yEnd));
                System.out.print(", start:(" + xStart + "," + yStart + "), mid:(" + xMid + "," + yMid + "), end:(" + xEnd + "," + yEnd + ")\n");
                // 检查结果
                if (xEnd >= aa.length && yEnd >= bb.length) {
                    System.out.println("found");
                    Snake current = snakes.get(0);
                    System.out.println(String.format("(%2d, %2d)<-(%2d, %2d)", current.getxEnd(), current.getyEnd(), current.getxStart(), current.getyStart()));
                    for (int i = 1; i < snakes.size(); i++) {
                        Snake tmp = snakes.get(i);
                        if (tmp.getxEnd() == current.getxStart()
                                && tmp.getyEnd() == current.getyStart()) {
                            current = tmp;
                            System.out.println(String.format("(%2d, %2d)<-(%2d, %2d)", current.getxEnd(), current.getyEnd(), current.getxStart(), current.getyStart()));
                            if (current.getxStart() == 0 && current.getyStart() == 0) {
                                break;
                            }
                        }
                    }
                    return;
                }
            }
        }

    }

    public static class Snake {

        private int xStart;

        private int yStart;

        private int xEnd;

        private int yEnd;

        public Snake(int xStart, int yStart, int xEnd, int yEnd) {
            this.xStart = xStart;
            this.yStart = yStart;
            this.xEnd = xEnd;
            this.yEnd = yEnd;
        }

        public int getxStart() {
            return xStart;
        }

        public void setxStart(int xStart) {
            this.xStart = xStart;
        }

        public int getyStart() {
            return yStart;
        }

        public void setyStart(int yStart) {
            this.yStart = yStart;
        }

        public int getxEnd() {
            return xEnd;
        }

        public void setxEnd(int xEnd) {
            this.xEnd = xEnd;
        }

        public int getyEnd() {
            return yEnd;
        }

        public void setyEnd(int yEnd) {
            this.yEnd = yEnd;
        }
    }
}

复制代码

参考文献

  1. Investigating Myers' diff algorithm: Part 1 of 2

  2. 简析Myers

  3. 论文

  4. 贪婪算法

相关文章
相关标签/搜索