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

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

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

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

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

1. 概述

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

2. DiffUtil的基本使用

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

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

??簡(jiǎn)單的了解Callback每個(gè)方法的作用之后,我們現(xiàn)在來(lái)看看DiffUtil是怎么使用的。
??我們先來(lái)看看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方法是通過(guò)id來(lái)判斷兩個(gè)Item是不是同一個(gè)Item,其次areContentsTheSame方法是通過(guò)判斷content來(lái)判斷兩個(gè)Item的內(nèi)容是否相同。
??然后,我們?cè)賮?lá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)行一些操作,來(lái)該改變數(shù)據(jù)源某些數(shù)據(jù)。請(qǐng)注意的是:所有的操作都必須在Adapter的數(shù)據(jù)源進(jìn)行操作,否則這里刷新完全沒(méi)有意義。正如上面的實(shí)現(xiàn),在變換之前,我先將源數(shù)據(jù)深拷貝到oldDataList數(shù)組,然后所有的變化操作都在mDataList數(shù)組(因?yàn)樗?code>Adapter的數(shù)據(jù)源,操作它才有意義),然后將改變之后的數(shù)據(jù)稱(chēng)為newDataList。
??如下便是DiffUtil的真正使用:

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

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


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

3. Myers算法

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

(1). 定義概念

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


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

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

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

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

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

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

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

(2). 算法

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

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

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

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

??從上面的代碼中,我們會(huì)有2個(gè)問(wèn)題:

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

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

A. 假設(shè)d = 3

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



??接下來(lái),我們將分情況來(lái)討論不同值的k。

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

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

??整個(gè)過(guò)程我們是很明白了,但是怎么用代碼來(lái)實(shí)現(xiàn)整個(gè)過(guò)程呢?
??需要我們知道的是,d(n)的計(jì)算基于d(n - 1)的計(jì)算,同時(shí)對(duì)于每個(gè)偶數(shù)d,我們?cè)谂紨?shù)k line上面去尋找snake的終點(diǎn),當(dāng)然這個(gè)尋找過(guò)程是基于上一條snake在奇數(shù)k line上面的終點(diǎn)(因?yàn)閗 是從k - 1或者 k + 1,推導(dǎo)出來(lái),如果k為偶數(shù),那么k - 1和k + 1肯定為奇數(shù))。
??我們假設(shè)一個(gè)V數(shù)組,其中k作為它的index,x作為它的值,y值可以由x 和k推導(dǎo)出來(lái)(k = x - y)。同時(shí)給定一個(gè)d值,k的范圍是 [-d, d],這個(gè)可以限制V數(shù)組的值的大小。
??我們必須從d = 0開(kāi)始,所以我們假設(shè)V[1] = 0,這個(gè)表示(k = 1,x = 0),所在點(diǎn)是(0, -1)。我們必須從(0, -1)向下移動(dòng),從而保證(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ù)組里面存儲(chǔ)的是在k line最新端點(diǎn)的坐標(biāo),所以為了尋找到所有的snake,我們?cè)赿的每次循環(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#寫(xiě)的,我這里將它簡(jiǎn)單改寫(xiě)稱(chēng)為Java。
??為什么這里會(huì)倒著來(lái)遍歷,也就是說(shuō),為什么從最后一條snake遍歷到第一條snake呢?最后一條snake肯定是我們想要的有效路徑的必經(jīng)之路,所以倒著來(lái)尋找snake,肯定是找到的snake都是在有效路徑上,因?yàn)閂s數(shù)組里面還有其他情況下的snake。

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

(1). DiffUtil生成DiffResult

??我相信,經(jīng)過(guò)上面的理解,大家在看DiffUtil的算法時(shí),應(yīng)該都能明白。DiffUtils代碼實(shí)現(xiàn)主要集中在diffPartial方法里面。
??diffPartial方法主要是來(lái)尋找一條snake,它的核心也就是Myers算法,這里我們將不分析了。calculateDiff方法是不斷的調(diào)用diffPartial方法,然后將尋找到的snake放入一個(gè)數(shù)組里面,最后就是創(chuàng)建一個(gè)DiffResult對(duì)象,將所有的snake作為參數(shù)傳遞過(guò)去。
??在DiffResult類(lèi)的內(nèi)部,分別有兩個(gè)數(shù)組來(lái)存儲(chǔ)狀態(tài),分別是:mOldItemStatuses,用來(lái)的舊Item的狀態(tài);mNewItemStatuses,用來(lái)存儲(chǔ)新Item的狀態(tài)。那么這兩個(gè)數(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,會(huì)計(jì)算move的操作,通常來(lái)說(shuō),我們都會(huì)設(shè)置為true。
??當(dāng)這里我們對(duì)DiffUtil生成DiffResult的過(guò)程已經(jīng)了解的差不多了,加下來(lái),我們?cè)谟懻撘粋€(gè)方法就是dispatchUpdatesTo方法

(2). DiffResult和Adapter

??整個(gè)DiffResult構(gòu)造完成之后,就需要將整個(gè)變化過(guò)程作用于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á)的意思非常簡(jiǎn)單,就是根據(jù)前面計(jì)算出來(lái)的mOldItemStatusesmNewItemStatuses數(shù)組來(lái)調(diào)用Adapter不同的方法。這里不同的就是,沒(méi)有直接調(diào)用Adapter的方法,而是使用了適配器模式,用AdapterListUpdateCallback來(lái)包裹了一下Adapter,然后通過(guò)AdapterListUpdateCallback的方法來(lái)調(diào)用Adapter的方法。
??這樣做有什么好處呢?在DiffUtil看到的不是Adapter,而是ListUpdateCallback接口,所以后面如果Adapter的API有啥變化,可以只改AdapterListUpdateCallback類(lèi),而不用更改DiffUtil類(lèi)。這樣設(shè)計(jì)非常的友好,同時(shí)我們?cè)谶@里可以學(xué)習(xí)到兩點(diǎn):

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

5. 總結(jié)

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

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

??如果不出意外的話,接下來(lái)我將分析LayoutManager

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

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

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