??首先,我估計(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。
??本文參考資料:
??本文有一部分的內(nèi)容來(lái)自上文的翻譯。我的建議是,各位同學(xué)可以直接看上面的文章,大佬的文章已經(jīng)將DiffUtil的核心算法講的非常透徹。
??本文打算從三個(gè)角度來(lái)分析DiffUtil
DiffUtil的基本使用Myers差量算法的深入探究DiffUtil的Myers算法實(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)用notifyItemRemoved、notifyItemInserted或者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ò)DiffUtil的calculateDiff方法來(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ú)非是remove和add(在這里,move操作和change操作我們將拆分為remove和add操作),這里就讓我想起來(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è)概念:
- snakes:一個(gè)snake表示向右或者向下走了一步,這個(gè)過(guò)程包括n個(gè)對(duì)角線。
- 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。- 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)題:
- 為什么 k的范圍是[-d, d]?
- 為什么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。
- 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)。
- 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。- 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)。- 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方法只做一件事情:更新mOldItemStatuses和mNewItemStatuses數(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)的mOldItemStatuses和mNewItemStatuses數(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):
- 適當(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。- 如果使用一個(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é)。
DiffUtil應(yīng)開(kāi)發(fā)者的需求產(chǎn)生,我們應(yīng)該去使用并且理解它。DiffUtil的差量計(jì)算采用的是Myers算法,具體的算法分析可以參考上面的描述。- 適當(dāng)?shù)氖褂眠m配器模式,可以減少一個(gè)類(lèi)去實(shí)現(xiàn)一些沒(méi)必要的接口。
??如果不出意外的話,接下來(lái)我將分析LayoutManager。
