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. 貪婪算法

相關文章
相關標籤/搜索