RecyclerView 源碼分析(六) - DiffUtil的差量算法分析

??首先,我估計(jì)有一部分的同學(xué)可能還不知道DiffUtil是什么,說實(shí)話,之前我也根本不了解這是什么東西。DiffUtil是我在公司實(shí)習(xí)的時(shí)候了解到的一個類,在那之前,我使用RecyclerView的方式也是大部分的人差不多,就是RecyclerView和它的四大組成部分任意組合。
??當(dāng)時(shí)在公司第一次看到這個東西的時(shí)候,立即兩眼發(fā)光,非常好奇這是什么東西,就好像在大街上看到美女一樣。后來在非工作時(shí)間的時(shí)候,我去了解了一下這個類,不過當(dāng)時(shí)也只是簡單的了解這個東西?,F(xiàn)在在系統(tǒng)的學(xué)習(xí)RecyclerView的源碼,我覺得有必要深入的了解和學(xué)習(xí)一下這個東西--DiffUtil。
??本文參考資料:

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

??本文有一部分的內(nèi)容來自上文的翻譯。我的建議是,各位同學(xué)可以直接看上面的文章,大佬的文章已經(jīng)將DiffUtil的核心算法講的非常透徹。
??本文打算從三個角度來分析DiffUtil

  1. DiffUtil的基本使用
  2. Myers差量算法的深入探究
  3. DiffUtilMyers算法實(shí)現(xiàn)以及DiffUtil怎么跟Adapter聯(lián)系起來的

1. 概述

??在正式分析DiffUtil之前,我們先來對DiffUtil有一個大概的了解--DiffUtil到底是什么東西。
??我們相信大家都遇到一種情況,那就是在一次操作里面可能會同時(shí)出現(xiàn)remove、add、change三種操作。像這種情況,我們不能調(diào)用notifyItemRemovednotifyItemInserted或者notifyItemChanged方法,為了視圖立即刷新,我們只能通過調(diào)用notifyDataSetChanged方法來實(shí)現(xiàn)。
??而notifyDataSetChanged方法有什么缺點(diǎn)呢?沒有動畫!對,通過調(diào)用notifyDataSetChanged方法來刷新視圖,每個操作是沒有動畫,這就很難受了!
??有沒有一種方式可以實(shí)現(xiàn)既能保留動畫,又能刷新動畫呢?我們單從解決問題的角度來說,我們可以設(shè)計(jì)一種算法,來比較變化前后的數(shù)據(jù)源有哪些變化,這里的變化包括,如上的三種操作。哪些位置進(jìn)行了change操作,哪些地方進(jìn)行了add操作,哪些地方進(jìn)行了remove操作,可以通過這種算法計(jì)算出來。
??Google爸爸考慮到這個問題大家都能遇到,那我?guī)湍銈儗?shí)現(xiàn),這樣你們就不用自己去實(shí)現(xiàn)了,這就是DiffUtil的由來。

2. DiffUtil的基本使用

??在正式分析DiffUtil的源碼之前,我們先來看看DiffUtil的基本使用,然后我們從基本使用入手,這樣看代碼的時(shí)候才不會迷茫。
??我們想要使用DiffUtil時(shí),有一個抽象類Callback是我們必須了解的,我們來看看,了解它的每個方法都都有什么作用。

方法名 作用
getOldListSize 原數(shù)據(jù)源的大小
getNewListSize 新數(shù)據(jù)源的大小
areItemsTheSame 判斷給定兩個Item的是否同一個Item。給定的是兩個Position,分別是原數(shù)據(jù)源的位置和新數(shù)據(jù)源的位置。判斷兩個Item是否是同一個Item,如果是不同的對象(新數(shù)據(jù)源和舊數(shù)據(jù)源持有的不是同一批對象,新數(shù)據(jù)源可能是從舊數(shù)據(jù)源那里深拷貝過來,也有重新進(jìn)行網(wǎng)絡(luò)請求返回的),可以給每個Item設(shè)置一個id,如果是同一個對象,可以直接使用==來判斷
areContentsTheSame 判斷給定的兩個Item內(nèi)容是否相同。只有areItemsTheSame返回為true,才會回調(diào)此方法。也就是說,只能當(dāng)兩個Item是同一個Item,才會調(diào)用此方法來判斷給定的兩個Item內(nèi)容是否相同。
getChangePayload 用于局部刷新,回調(diào)此方法表示所給定的位置肯定進(jìn)行change操作,所以這里不需要判斷是否為change操作。

??簡單的了解Callback每個方法的作用之后,我們現(xiàn)在來看看DiffUtil是怎么使用的。
??我們先來看看ItemCallback是怎么實(shí)現(xiàn)的:

public class RecyclerItemCallback extends DiffUtil.Callback {

    private List<Bean> mOldDataList;
    private List<Bean> mNewDataList;

    public RecyclerItemCallback(List<Bean> oldDataList, List<Bean> newDataList) {
        this.mOldDataList = oldDataList;
        this.mNewDataList = newDataList;
    }

    @Override
    public int getOldListSize() {
        return mOldDataList.size();
    }

    @Override
    public int getNewListSize() {
        return mNewDataList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return Objects.equals(mNewDataList.get(newItemPosition).getId(), mOldDataList.get(oldItemPosition).getId());
    }

    @Override
    public boolean areContentsTheSame(int i, int i1) {
        return Objects.equals(mOldDataList.get(i).getContent(), mNewDataList.get(i1).getContent());
    }
}

??這里,areItemsTheSame方法是通過id來判斷兩個Item是不是同一個Item,其次areContentsTheSame方法是通過判斷content來判斷兩個Item的內(nèi)容是否相同。
??然后,我們再來看看DiffUtil是怎么使用的:

    private void refreshData() {
        final List<Bean> oldDataList = new ArrayList<>();
        final List<Bean> newDataList = mDataList;

        // deep copy
        for (int i = 0; i < mDataList.size(); i++) {
            oldDataList.add(mDataList.get(i).deepCopy());
        }
        // change
        for (int i = 0; i < newDataList.size(); i++) {
            if (i % 5 == 0) {
                newDataList.get(i).setContent("change data = " + i);
            }
        }
        // remove
         newDataList.remove(0);
         newDataList.remove(0);
        // add
        addData(5, newDataList);
        // diffUtil
        RecyclerItemCallback recyclerItemCallback = new RecyclerItemCallback(oldDataList, newDataList);
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(recyclerItemCallback, false);
        diffResult.dispatchUpdatesTo(mRecyclerAdapter);
    }

??這里我們進(jìn)行一些操作,來該改變數(shù)據(jù)源某些數(shù)據(jù)。請注意的是:所有的操作都必須在Adapter的數(shù)據(jù)源進(jìn)行操作,否則這里刷新完全沒有意義。正如上面的實(shí)現(xiàn),在變換之前,我先將源數(shù)據(jù)深拷貝到oldDataList數(shù)組,然后所有的變化操作都在mDataList數(shù)組(因?yàn)樗?code>Adapter的數(shù)據(jù)源,操作它才有意義),然后將改變之后的數(shù)據(jù)稱為newDataList
??如下便是DiffUtil的真正使用:

        RecyclerItemCallback recyclerItemCallback = new RecyclerItemCallback(oldDataList, newDataList);
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(recyclerItemCallback, false);
        diffResult.dispatchUpdatesTo(mRecyclerAdapter);

??上面便是使用DiffUtil的固定步驟:顯示創(chuàng)建ItemCallback的對象,然后通過DiffUtilcalculateDiff方法來進(jìn)行差量計(jì)算,最后就是調(diào)用dispatchUpdatesTo方法進(jìn)行notify操作。
??整個過程還是比較簡單的,我們來看看展示效果:


??了解完DiffUtil是怎么使用,接下來我們將正式DiffUtil的差量計(jì)算算法,如果還有同學(xué)不明白DiffUtil怎么使用,可以到我的github下載上面的Demo: DiffUtilDemo

3. Myers算法

??DiffUtil進(jìn)行差量計(jì)算采用的是著名的Myers算法。對于我們這種移動開發(fā)的菜逼,很少接觸到算法,所以知道這個算法的同學(xué)應(yīng)該比較少,況且還深入了解它。當(dāng)然大家不要怕,本文會詳細(xì)的介紹Myers算法,包括它的理論和實(shí)現(xiàn)。放心吧,這個算法比較簡單,我覺得比看毛片算法還簡單。
??本部分的大部分內(nèi)容來自于Investigating Myers' diff algorithm: Part 1 of 2這篇文章,有興趣的同學(xué)可以直接看這篇文章。

(1). 定義概念

??我們先來簡單分析一下我們需要達(dá)到的目的。比如說有A數(shù)組和B數(shù)組,我們想要達(dá)到的目的就是,從A數(shù)組變成B數(shù)組,分別要進(jìn)行哪些操作。這些操作里面無非是removeadd(在這里,move操作和change操作我們將拆分為removeadd操作),這里就讓我想起來算法題中一道題是編輯距離編輯距離的意思就是:從A字符串變成B字符串的最小操作步數(shù),這里的操作就是上面的兩種操作,有興趣的可以看我之前的一篇文章:Java 算法-編輯距離(動態(tài)規(guī)劃)。
??我們就可以求解從A數(shù)組變成B數(shù)組的問題轉(zhuǎn)換成為求解從A字符串變成B字符串的問題(其實(shí),字符串就是字符數(shù)組)。
??我們一步一步的分析這個問題,我們假設(shè)A字符串為ABCABBA,B字符串為CBABAC。然后我們可以得到下面的一個二維數(shù)組(如下的軟件連接:DiffTutorial)。


??從上面的圖片中,我們可以看出來,我們假設(shè)X軸是原字符串,Y軸是新字符串。其中,這個問題的目的就是我們需要從點(diǎn)(0,0)(原點(diǎn))到點(diǎn)(m,n)(終點(diǎn))的最短路徑,學(xué)過基本算法的同學(xué)應(yīng)該都知道,這個就是回溯法的基本操作。
??然后我們在來看一張圖片:

??這張圖片相對于上面的圖片,就是多了一些對角線。我們知道要想求解從(0,0)到(m,n)的最短路徑,我們只能往右或者往下走,因?yàn)橥匣蛘咄笞叨际窃诶@路。而多了對角線之后,我們還可以走對角線,如果能走對角線,相對于往右或者往下走的話,就更加的近了。那這些對角線的是按照什么規(guī)則畫出來的呢?
??其實(shí)非常的簡單,我們就從左往右,從上往下掃描整個二維數(shù)組,如果當(dāng)前位置的x表示的字符跟y表示的字符相同的話,就畫一條對角線(從左上到右下)。從這里,我們就可以看出來,我們想要的答案就是路徑里面盡可能包含多的對角線。

這里,我們簡單的定義一下,向右走一個格子或者向下走一個格子表示一步,而走一條對角線不計(jì)入步數(shù)。

??我們假設(shè)向右移動一步表示從A字符串中remove刪除一個字符,向下移動一步表示向B字符串a(chǎn)dd一個字符。
??在分析尋找路徑的算法之前,我們先來定義幾個概念:

  1. snakes:一個snake表示向右或者向下走了一步,這個過程包括n個對角線。
  2. k lines: k lines表示長的對角線,其中每個k = x - y。假設(shè)一個點(diǎn)m(m,n),那么它所在的k line值為m - n。如圖:

    ??其中橙色線表示偶數(shù)k line,棕色線表示奇數(shù)k line。
  3. d contours:每條有效路徑(能從(0,0)到(m,n)的路徑都稱為有效路徑)的步數(shù)。形象點(diǎn),一條path有多個snake組成,這里d contours表示snake的個數(shù)。

??如上就是我們定義幾個概念。其中,如果向右走的話,k會+1,向下走,k會-1。

(2). 算法

??我們想要的答案尋找最短的有效路徑,那么就是尋找d contours最小的路徑。那么我們很容易的能實(shí)現(xiàn)一個循環(huán),用來找到最小路徑:

for ( int d = 0 ; d <= m + n ; d++ )

??我們從0開始遍歷,只要能第一次找到有效路徑,那么這條路徑就是我們需要的答案。那么最大值為什么是m + n呢?假設(shè)這個過程沒有對角線,只能向下或者向右走,那么最終會有m + nsnake(向下一步或者向右一步就是一個snake),所以d的最大值是 m + n。
??而在內(nèi)循環(huán)里面,我們需要遍歷在每種d值,經(jīng)過了哪些k lines,所以內(nèi)循環(huán)應(yīng)該來遍歷k lines。這里我先將內(nèi)循環(huán)的代碼寫出來,然后再解釋幾個問題。

for ( int k = -d ; k <= d ; k += 2 )

??從上面的代碼中,我們會有2個問題:

  1. 為什么 k的范圍是[-d, d]?
  2. 為什么k每次+2,而不是+1?

??針對上面的問題,我進(jìn)行一一的解答。首先來看看第一個問題。
??k = -d,全部都向下走,因?yàn)橐还瞕步,一共會向下走d步,所以k為-d(向下走,k會-1);當(dāng)然,k = d就表示全部都向右走。
??再來看看第二個問題吧。
??根據(jù)我們的觀察,如果終點(diǎn)所在的k line是偶數(shù),那么最終的步數(shù)d也是偶數(shù),反之亦然。這幾句話是什么意思呢?這樣來說吧,如果我們經(jīng)過d步就能到達(dá)終點(diǎn),那么如果d為偶數(shù),終點(diǎn)所在k line也為偶數(shù),奇數(shù)也是一樣的道理。所以k直接+2就行了,不用加1。
??理解了這些的問題,現(xiàn)在我們需要解決的問題是,給定一個k值,怎么來尋找有效路徑?
??給定的k值,我們從k + 1向下移動一步或者從k - 1向右移動一步,然后我們就可以基于這個規(guī)則來解決我們的問題。
??這里,我們用一個例子來看一下具體是怎么解決問題的。

A. 假設(shè)d = 3

??如果d為3,那么k的取值范圍是[-3,-1,1,-3] (根據(jù)上面的內(nèi)循環(huán)得到的)。為了方便理解,我將所有的snake記錄成一張表,如圖:



??接下來,我們將分情況來討論不同值的k。

  1. k = -3:這種情況下,只有當(dāng)k = -2,d = 2時(shí),向下移動一步(k = -4, d = 2這種情況不存在)。所以,我們可以這么來走,從(2,4)點(diǎn)開始向下走到(2,5),由于(2,5)和(3,6)之間存在一個對角線,可以走到(3,6)。所以著整個snake是:(2,4) -> (2,5) -> (3,6)。
  2. k = -1:當(dāng)k = -1時(shí),有兩種情況需要來討論:分別是k = 0,d = 2向下走;k = -2 ,d = 2向右走。
    ??當(dāng)k = 0,d = 2時(shí),是(2,2)點(diǎn)。所以當(dāng)從(2,2)點(diǎn)向下走一步,到達(dá)(2,3),由于(2,3)沒有對角線連接,所以整個snake是:(2,3) -> (2,4)。
    ??當(dāng)k = -2 ,d = 2時(shí),是(2,4)點(diǎn)。所以當(dāng)從(2,4)點(diǎn)向右走一步,到達(dá)(2,5),由于(2,5)與(3,6)存在對角線,所以整個snake是:(2,4) -> (2,5) -> (3,6)。
    在整個過程中,存在兩條snake,我們選擇是沿著k line走的最遠(yuǎn)的snake,所以選擇第二條snake。
  3. k = 1:當(dāng)k = 1時(shí),存在兩種可能性,分別是從k = 0向右走一步,或者k = 2向下走一步,我們分別來討論一下。
    ??當(dāng)k = 0,d = 2時(shí),是(2,2)點(diǎn)。所以當(dāng)從(2,2)向右走一步,到達(dá)(3,2),由于(3,2)與(5,4)存在對角線,所以整個snake是:(2,2) ->(3,2) ->(5,4)。
    ??當(dāng)k = 2,d = 2時(shí),是(3,1)點(diǎn)。所以當(dāng)從(3,1)向下走一步,到達(dá)(3,2)。所以這個snake是:(3,1) ->(3,2) ->(5,4)。
    ??在整個過程中,存在兩條snake,我們選擇起點(diǎn)x值較大的snake,所以是:(3,1) ->(3,2) ->(5,4)。
  4. k = 3:這種情況下,(k = 4, d = 2)是不可能的,所以我們必須在(k = 2,d = 2)時(shí)向右移動一步。當(dāng)k = 2, d = 2時(shí), 是(3,1)點(diǎn)。所以從(3,1)點(diǎn)向右移動一步是(4,1)點(diǎn)。所以整個snake是:(3,1) -> (4,1) -> (5,2).

B. 算法實(shí)現(xiàn)

??整個過程我們是很明白了,但是怎么用代碼來實(shí)現(xiàn)整個過程呢?
??需要我們知道的是,d(n)的計(jì)算基于d(n - 1)的計(jì)算,同時(shí)對于每個偶數(shù)d,我們在偶數(shù)k line上面去尋找snake的終點(diǎn),當(dāng)然這個尋找過程是基于上一條snake在奇數(shù)k line上面的終點(diǎn)(因?yàn)閗 是從k - 1或者 k + 1,推導(dǎo)出來,如果k為偶數(shù),那么k - 1和k + 1肯定為奇數(shù))。
??我們假設(shè)一個V數(shù)組,其中k作為它的index,x作為它的值,y值可以由x 和k推導(dǎo)出來(k = x - y)。同時(shí)給定一個d值,k的范圍是 [-d, d],這個可以限制V數(shù)組的值的大小。
??我們必須從d = 0開始,所以我們假設(shè)V[1] = 0,這個表示(k = 1,x = 0),所在點(diǎn)是(0, -1)。我們必須從(0, -1)向下移動,從而保證(0,0)是必經(jīng)之地。

V[ 1 ] = 0;
for ( int d = 0 ; d <= N + M ; d++ )
{
  for ( int k = -d ; k <= d ; k += 2 )
  {
    // down or right?
    bool down = ( k == -d || ( k != d && V[ k - 1 ] < V[ k + 1 ] ) );

    int kPrev = down ? k + 1 : k - 1;

    // start point
    int xStart = V[ kPrev ];
    int yStart = xStart - kPrev;

    // mid point
    int xMid = down ? xStart : xStart + 1;
    int yMid = xMid - k;

    // end point
    int xEnd = xMid;
    int yEnd = yMid;

    // follow diagonal
    int snake = 0;
    while ( xEnd < N && yEnd < M && A[ xEnd ] == B[ yEnd ] ) { xEnd++; yEnd++; snake++; }

    // save end point
    V[ k ] = xEnd;

    // check for solution
    if ( xEnd >= N && yEnd >= M ) /* solution has been found */
  }
}

??上面的代碼尋找一條到達(dá)終點(diǎn)的snake。因?yàn)閂數(shù)組里面存儲的是在k line最新端點(diǎn)的坐標(biāo),所以為了尋找到所有的snake,我們在d的每次循環(huán)完畢之后,從d(Solution)遍歷到0。如下:

List<int[]> Vs; // saved V's indexed on d
List<Snake> snakes; // list to hold solution

Point p = new Point(n, n); // start at the end

for ( int d = vs.Count - 1 ; p.X > 0 || p.Y > 0 ; d-- )
{
  int[] V = Vs[d];

  int k = p.X - p.Y;

  // end point is in V
  int xEnd = V[k];
  int yEnd = x - k;

  // down or right?
  bool down = ( k == -d || ( k != d && V[ k - 1 ] < V[ k + 1 ] ) );

  int kPrev = down ? k + 1 : k - 1;

  // start point
  int xStart = V[ kPrev ];
  int yStart = xStart - kPrev;

  // mid point
  int xMid = down ? xStart : xStart + 1;
  int yMid = xMid - k;

  snakes.Insert( 0, new Snake( /* start, mid and end points */ ) );

  p.X = xStart;
  p.Y = yStart;
}

??Investigating Myers' diff algorithm: Part 1 of 2文章是用C#寫的,我這里將它簡單改寫稱為Java。
??為什么這里會倒著來遍歷,也就是說,為什么從最后一條snake遍歷到第一條snake呢?最后一條snake肯定是我們想要的有效路徑的必經(jīng)之路,所以倒著來尋找snake,肯定是找到的snake都是在有效路徑上,因?yàn)閂s數(shù)組里面還有其他情況下的snake。

4. DiffUtil的實(shí)現(xiàn)

(1). DiffUtil生成DiffResult

??我相信,經(jīng)過上面的理解,大家在看DiffUtil的算法時(shí),應(yīng)該都能明白。DiffUtils代碼實(shí)現(xiàn)主要集中在diffPartial方法里面。
??diffPartial方法主要是來尋找一條snake,它的核心也就是Myers算法,這里我們將不分析了。calculateDiff方法是不斷的調(diào)用diffPartial方法,然后將尋找到的snake放入一個數(shù)組里面,最后就是創(chuàng)建一個DiffResult對象,將所有的snake作為參數(shù)傳遞過去。
??在DiffResult類的內(nèi)部,分別有兩個數(shù)組來存儲狀態(tài),分別是:mOldItemStatuses,用來的舊Item的狀態(tài);mNewItemStatuses,用來存儲新Item的狀態(tài)。那么這兩個數(shù)組是在哪里被賦值呢?答案就在findMatchingItems方法(在DiffResult的構(gòu)造方法里面調(diào)用):

        private void findMatchingItems() {
            int posOld = mOldListSize;
            int posNew = mNewListSize;
            // traverse the matrix from right bottom to 0,0.
            for (int i = mSnakes.size() - 1; i >= 0; i--) {
                final Snake snake = mSnakes.get(i);
                final int endX = snake.x + snake.size;
                final int endY = snake.y + snake.size;
                if (mDetectMoves) {
                    while (posOld > endX) {
                        // this is a removal. Check remaining snakes to see if this was added before
                        findAddition(posOld, posNew, i);
                        posOld--;
                    }
                    while (posNew > endY) {
                        // this is an addition. Check remaining snakes to see if this was removed
                        // before
                        findRemoval(posOld, posNew, i);
                        posNew--;
                    }
                }
                for (int j = 0; j < snake.size; j++) {
                    // matching items. Check if it is changed or not
                    final int oldItemPos = snake.x + j;
                    final int newItemPos = snake.y + j;
                    final boolean theSame = mCallback
                            .areContentsTheSame(oldItemPos, newItemPos);
                    final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED;
                    mOldItemStatuses[oldItemPos] = (newItemPos << FLAG_OFFSET) | changeFlag;
                    mNewItemStatuses[newItemPos] = (oldItemPos << FLAG_OFFSET) | changeFlag;
                }
                posOld = snake.x;
                posNew = snake.y;
            }
        }

??findMatchingItems方法的具體細(xì)節(jié)這里我們就不討論了,其中findMatchingItems方法只做一件事情:更新mOldItemStatusesmNewItemStatuses數(shù)組。同時(shí)如果mDetectMoves為true,會計(jì)算move的操作,通常來說,我們都會設(shè)置為true。
??當(dāng)這里我們對DiffUtil生成DiffResult的過程已經(jīng)了解的差不多了,加下來,我們在討論一個方法就是dispatchUpdatesTo方法

(2). DiffResult和Adapter

??整個DiffResult構(gòu)造完成之后,就需要將整個變化過程作用于Adapter的更新,也就是dispatchUpdatesTo方法調(diào)用。

        public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
            final BatchingListUpdateCallback batchingCallback;
            if (updateCallback instanceof BatchingListUpdateCallback) {
                batchingCallback = (BatchingListUpdateCallback) updateCallback;
            } else {
                batchingCallback = new BatchingListUpdateCallback(updateCallback);
                // replace updateCallback with a batching callback and override references to
                // updateCallback so that we don't call it directly by mistake
                //noinspection UnusedAssignment
                updateCallback = batchingCallback;
            }
            // These are add/remove ops that are converted to moves. We track their positions until
            // their respective update operations are processed.
            final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
            int posOld = mOldListSize;
            int posNew = mNewListSize;
            for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
                final Snake snake = mSnakes.get(snakeIndex);
                final int snakeSize = snake.size;
                final int endX = snake.x + snakeSize;
                final int endY = snake.y + snakeSize;
                if (endX < posOld) {
                    dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
                }

                if (endY < posNew) {
                    dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
                            endY);
                }
                for (int i = snakeSize - 1; i >= 0; i--) {
                    if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
                        batchingCallback.onChanged(snake.x + i, 1,
                                mCallback.getChangePayload(snake.x + i, snake.y + i));
                    }
                }
                posOld = snake.x;
                posNew = snake.y;
            }
            batchingCallback.dispatchLastEvent();
        }

??dispatchUpdatesTo方法看上去比較難,其實(shí)表達(dá)的意思非常簡單,就是根據(jù)前面計(jì)算出來的mOldItemStatusesmNewItemStatuses數(shù)組來調(diào)用Adapter不同的方法。這里不同的就是,沒有直接調(diào)用Adapter的方法,而是使用了適配器模式,用AdapterListUpdateCallback來包裹了一下Adapter,然后通過AdapterListUpdateCallback的方法來調(diào)用Adapter的方法。
??這樣做有什么好處呢?在DiffUtil看到的不是Adapter,而是ListUpdateCallback接口,所以后面如果Adapter的API有啥變化,可以只改AdapterListUpdateCallback類,而不用更改DiffUtil類。這樣設(shè)計(jì)非常的友好,同時(shí)我們在這里可以學(xué)習(xí)到兩點(diǎn):

  1. 適當(dāng)?shù)氖褂眠m配器模式,將一些操作封裝到適配器類里面,當(dāng)依賴類的API有所改變,我們只需改變適配器類就行,而不用更改那么復(fù)雜的類,因?yàn)閺?fù)雜類更改起來非常的麻煩。在這里,依賴類是Adapter,復(fù)雜類是DiffUtil。
  2. 如果使用一個類,但是必須保證這個類實(shí)現(xiàn)某個接口。我們不妨使用適配器模式,設(shè)計(jì)一個適配器類來實(shí)現(xiàn)接口,在適配器操作想要使用的那個類。這樣能避免每個類去實(shí)現(xiàn)沒必要的接口,在這里Adapter就沒必要實(shí)現(xiàn)ListUpdateCallback的接口,所以可以使用適配器模式來包裹一下Adapter就行了。

5. 總結(jié)

??到這里,我們對DiffUtil的算法已經(jīng)有一定的理解了,最后,我再對此進(jìn)行簡單的總結(jié)。

  1. DiffUtil應(yīng)開發(fā)者的需求產(chǎn)生,我們應(yīng)該去使用并且理解它。
  2. DiffUtil的差量計(jì)算采用的是Myers算法,具體的算法分析可以參考上面的描述。
  3. 適當(dāng)?shù)氖褂眠m配器模式,可以減少一個類去實(shí)現(xiàn)一些沒必要的接口。

??如果不出意外的話,接下來我將分析LayoutManager

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容