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;
}
}
}
複製代碼