??首先,我估計(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。
??本文參考資料:
??本文有一部分的內(nèi)容來自上文的翻譯。我的建議是,各位同學(xué)可以直接看上面的文章,大佬的文章已經(jīng)將DiffUtil的核心算法講的非常透徹。
??本文打算從三個角度來分析DiffUtil
DiffUtil的基本使用Myers差量算法的深入探究DiffUtil的Myers算法實(shí)現(xiàn)以及DiffUtil怎么跟Adapter聯(lián)系起來的
1. 概述
??在正式分析DiffUtil之前,我們先來對DiffUtil有一個大概的了解--DiffUtil到底是什么東西。
??我們相信大家都遇到一種情況,那就是在一次操作里面可能會同時(shí)出現(xiàn)remove、add、change三種操作。像這種情況,我們不能調(diào)用notifyItemRemoved、notifyItemInserted或者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的對象,然后通過DiffUtil的calculateDiff方法來進(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)行哪些操作。這些操作里面無非是remove和add(在這里,move操作和change操作我們將拆分為remove和add操作),這里就讓我想起來算法題中一道題是編輯距離。編輯距離的意思就是:從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一個字符。
??在分析尋找路徑的算法之前,我們先來定義幾個概念:
- snakes:一個snake表示向右或者向下走了一步,這個過程包括n個對角線。
- k lines: k lines表示長的對角線,其中每個k = x - y。假設(shè)一個點(diǎn)m(m,n),那么它所在的k line值為m - n。如圖:
??其中橙色線表示偶數(shù)k line,棕色線表示奇數(shù)k line。- 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 + n 個snake(向下一步或者向右一步就是一個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個問題:
- 為什么 k的范圍是[-d, d]?
- 為什么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。
- 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)。
- 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。- 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)。- 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方法只做一件事情:更新mOldItemStatuses和mNewItemStatuses數(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ì)算出來的mOldItemStatuses和mNewItemStatuses數(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):
- 適當(dāng)?shù)氖褂眠m配器模式,將一些操作封裝到適配器類里面,當(dāng)依賴類的API有所改變,我們只需改變適配器類就行,而不用更改那么復(fù)雜的類,因?yàn)閺?fù)雜類更改起來非常的麻煩。在這里,依賴類是
Adapter,復(fù)雜類是DiffUtil。- 如果使用一個類,但是必須保證這個類實(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é)。
DiffUtil應(yīng)開發(fā)者的需求產(chǎn)生,我們應(yīng)該去使用并且理解它。DiffUtil的差量計(jì)算采用的是Myers算法,具體的算法分析可以參考上面的描述。- 適當(dāng)?shù)氖褂眠m配器模式,可以減少一個類去實(shí)現(xiàn)一些沒必要的接口。
??如果不出意外的話,接下來我將分析LayoutManager。
