RecyclerView 配合 DiffUtil,好用到飛起

承香墨影發表於2019-03-04

版權宣告:

本賬號釋出文章均來自公眾號,承香墨影(cxmyDev),版權歸承香墨影所有。

每週會統一更新到這裡,如果喜歡,可關注公眾號獲取最新文章。

未經允許,不得轉載。

一、前言

DIffUtils 是 Support-v7:24:2.0 中,更新的工具類。因為已經更新了一段時間了,也不好說是最新更新的。

它主要是為了配合 RecyclerView 使用,通過比對新、舊兩個資料集的差異,生成舊資料到新資料的最小變動,然後對有變動的資料項,進行區域性重新整理。

接下來就 DiffUtil 的使用細節,進行一個詳細的講解,希望一篇文章就完全理解 DiffUtil。

二、為什麼會有DiffUtil

RecyclerView 自從被髮布以來,一直被說成是 ListView、GridView 等一系列列表控制元件的完美替代品。並且它本身使用起來也非常的好用,佈局切換方便、自帶 ViewHolder 、區域性更新並且可帶更新動畫等等。

區域性更新、並且可以很方便的設定更新動畫這一點,是 RecyclerView 一個不錯的亮點。它為此提供了對應的方法:

  • adapter.notifyItemChange()
  • adapter.notifyItemInserted()
  • adapter.notifyItemRemoved()
  • adapter.notifyItemMoved();

以上方法都是為了對資料集中,單一項進行操作,並且為了操作連續的資料集的變動,還提供了對應的 notifyRangeXxx() 方法。

雖然 RecyclerView 提供的區域性更新的方法,看似非常的好用,但是實際上,其實並沒有什麼用。

在實際開發中,最方便的做法就是無腦呼叫 notifyDataSetChanged(),用於更新 Adapter 的資料集。

雖然 notifyDataSetChanged() 有一些缺點:

  • 不會觸發 RecyclerView 的區域性更新的動畫。
  • 效能低,會重新整理整個 RecyclerView 可視區域。

但是真有需要頻繁重新整理,前後兩個資料集的場景。

方案一:使用一個 notifyDataSetChanged() 方法。

方案二:自己寫一個資料集比對方法,然後去計算他們的差值,最後呼叫對應的方法更新到 RecyclerView 中去。

我這麼懶,如果不是必要,當然是會選 方案一 了。畢竟和之前 ListView 的時候,也沒有更差了。

Google 顯然也發現了這個問題,所以 DiffUtil 被髮布了。

三、介紹DiffUtil

就像前面說的,DiffUtil 就是為了解決這個痛點的。它能很方便的對兩個資料集之間進行比對,然後計算出變動情況,配合 RecyclerView.Adapter ,可以自動根據變動情況,呼叫 Adapter 的對應方法。

當然,DiffUtil 不僅只能配合 RecyclerView 使用,它實際上可以單獨用於比對兩個資料集,然後如何操作是可以定製的,那麼在什麼場景下使用,就全憑我們自己發揮了。

DiffUtil 在使用起來,主要需要關注幾個類:

  • DiffUtil.Callback:具體用於限定資料集比對規則。
  • DiffUtil.DiffResult:比對資料集之後,返回的差異結果。

1、DiffUtil.Callback

DiffUtil.Callback 主要就是為了限定兩個資料集中,子項的比對規則。畢竟開發者面對的資料結構多種多樣,既然沒法做一套通用的內容比對方式,那麼就將比對的規則,交還給開發者來實現即可。

在 Callback 中,其實只需要實現 4 個方法:

  • getOldListSize():舊資料集的長度。
  • getNewListSize():新資料集的長度
  • areItemsTheSame():判斷是否是同一個Item。
  • areContentsTheSame():如果是通一個Item,此方法用於判斷是否同一個 Item 的內容也相同。

前兩個是獲取資料集長度的方法,這沒什麼好說的。但是後兩個方法,主要是為了對應多佈局的情況產生的,也就是存在多個 viewType 和多個 ViewHodler 的情況。首先需要使用 areItemsTheSame() 方法比對是否來自同一個 viewType(也就是同一個 ViewHolder ) ,然後再通過 areContentsTheSame() 方法比對其內容是否也相等。

其實 Callback 還有一個 getChangePayload() 的方法,它可以在 ViewType 相同,但是內容不相同的時候,用 payLoad 記錄需要在這個 ViewHolder 中,具體需要更新的View。

areItemsTheSame()areContentsTheSame()getChangePayload() 分別代表了不同量級的重新整理。

首先會通過 areItemsTheSame() 判斷當前 position 下,ViewType 是否一致,如果不一致就表明當前 position 下,從資料到 UI 結構上全部變化了,那麼就不關心內容,直接更新就好了。如果一致的話,那麼其實 View 是可以複用的,就還需要再通過 areContentsTheSame() 方法判斷其內容是否一致,如果一致,則表示是同一條資料,不需要做額外的操作。但是一旦不一致,則還會呼叫 getChangePayload() 來標記到底是哪個地方的不一樣,最終標記需要更新的地方,最終返回給 DiffResult 。

當然,對效能要是要求沒那麼高的情況下,是可以不使用 getChangedPayload() 方法的。

2、DiffUtil.DiffResult

DiffUtil.DiffResult 其實就是 DiffUtil 通過 DiffUtil.Callback 計算出來,兩個資料集的差異。它是可以直接使用在 RecyclerView 上的。如果有必要,也是可以通過實現 ListUpdateCallback 介面,來比對這些差異的。

3、使用DiffUtil

介紹了 Callback 和 DiffResult 之後,其實就可以正常使用 DiffUtil 來進行資料集的比對了。

在這個過程中,其實真的很簡單,只需要呼叫兩個方法:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(oldDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);複製程式碼

calculateDiff 方法主要是用於通過一個具體的 DiffUtils.Callback 實現物件,來計算出兩個資料集差異的結果,得到 DiffUtil.DiffResult

而 calculateDiff 的另外一個引數,用於標記是否需要檢測 Item 的移動。

DiffUtil 使用的是 Eugene Myers 的差別演算法,這個演算法本身是不檢查元素的移動的。也就是說,有元素的移動它也只是會先標記為刪除,然後再標記插入。而如果需要計算元素的移動,它實際上也是在通過 Eugene Myers 演算法比對之後,再進行一次移動檢查。所以,如果集合本身已經排序過了,可以不進行移動的檢查。

dispatchUpdatesTo() 就是將這個資料集差異的結果,通過 Adapter 更新到 RecyclerView 上面。

實際上 dispatchUpdatesTo(Adapter) ,也是使用的 ListUpdateCallback 這個介面,在其中獲得差異,然後呼叫 Adapter 的對應方法。

diff-dut
diff-dut

四、上例子

既然已經說清楚了,那麼我們開始上例子了。

功能很簡單,有四個資料集,使用 RecyclerView 承載,然後有一個按鈕,用於輪換的切換資料集。

1、實現 DiffUtil.Callback

為了簡單,RecyclerView 中使用單一 ViewType ,並且使用一個 TextView 承載一個 字串來顯示。

那麼我們開始實現 Callback:

diff-callback
diff-callback

2、切換資料集

既然已經有了 DiffUtil.Callback 的實現之後,我們就需要對切換資料集的點選事件進行處理了。

diff-change
diff-change

3、實現效果

關鍵程式碼已經貼出來了,其實非常的簡單,最終執行的效果如下:

五、DiffUtil 效率問題

既然 DiffUtil 非常的好用,並且內部也實現了一套演算法,但是我們也需要關心它的效率問題。

根據 Google 官方文件中給出的例子,在 Nexus 5X M 系統上,DiffUtil 的效率問題,給出了一些參考的資料:

diff-duibi
diff-duibi

可以看到,實際上,DiffUtil 的演算法把效率問題解決的非常的好。在開啟計算移動的情況下,1000 條資料中有 200 個修改,平均值也只有 13.54 ms ,基本上都是毫秒級的。

Google 官方同時也指出,如果是對大資料集的比對,最好是方在子執行緒中去完成計算,也就是其實是存在堵塞 UI 的情況的。所以如果你遇見了使用 DiffUtil 之後,每次重新整理有卡頓的情況,可以考慮是否資料集太大,是否應該在子執行緒中完成計算。

六、結語

DiffUtil 已經介紹完了,如果覺得本文對你有幫助。都看到這裡了,點個贊再走吧。

公眾號二維碼.jpg
公眾號二維碼.jpg

相關文章