RecyclerView中有许多神奇的特性,好比局部刷新,它不只能够针对某个item进行刷新,也能够针对item中的某些数据进行刷新。这对咱们页面的页面渲染带来了很大的提高。那么RecyclerView是怎么经过对新旧数据的对比来作到局部刷新的?更进一步,对比新旧数据的这个Diff算法又是什么的样子的。下面将会从这两个部分来展开讨论。git
RecyclerView为了应对局部刷新提供了下面几个方法:github
为了对数据集的改动还提供了notifyRange**系列的方法。可是这些方法在DiffUtil类出现以前其实使用的场景颇有限。大部分状况都是直接的调用notifyDataSetChanged()方法。可是这个方法有一些明显的缺点:算法
上面说到其实RecyclerView是有提供局部刷新的方法的,可是这些方法不多被用到,缘由是咱们没有一个高效处理新旧数据差别的方法,直到DiffUtil类的出现。使用DiffUtil的话只须要关注两个类:数据库
咱们须要经过这个类实现咱们的diff规则。它有四个方法:api
下面简单的介绍一下方法3和方法4,areItemsTheSame判断是不是同一个item,通常使用item数据中的惟一id来判断,例如数据库中的id。areContentsTheSame 这个方法是在方法3返回true的时候才会被调用。要解决的问题是item中某些字段内容的刷新。例如朋友圈中更新点赞的状态。实际上是不须要对整个item进行刷新的,只须要点赞所对应的控件进行刷新。bash
上面这些内容会有个demo帮助理解工具
在上面使用RecyclerView作局部刷新的时候,使用了一个DiffUtil工具类。那么这个工具类是基于什么样的算法实现的? 要讲明白这个问题需先介绍一些概念post
什么是diff diff是两个数据之间的区别(这里的数据能够是文件,字符串等等),也就是将源数据变成目标数据所须要的操做。咱们研究diff算法就是找到一种操做最少的方式。动画
Myers Diff算法ui
DiffUtil这个工具类使用的Diff算法来自于Eugene W. Myers在1986年发表的一篇算法论文
算法依赖于新旧数据(定义为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表示横(竖)方向移动一步后接着尽量多的斜边方向的移动,组成的线
k = x - y定义的一条直线,也就是k相同的点组成的一条直线。k线之间是相互平行的斜线。
移动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;
}
}
}
复制代码