版權聲明:
本賬號發布文章均來自公眾號,承香墨影(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 的對應方法。
四、上例子
既然已經說清楚了,那么我們開始上例子了。
功能很簡單,有四個數據集,使用 RecyclerView 承載,然后有一個按鈕,用於輪換的切換數據集。
1、實現 DiffUtil.Callback
為了簡單,RecyclerView 中使用單一 ViewType ,並且使用一個 TextView 承載一個 字符串來顯示。
那么我們開始實現 Callback:
2、切換數據集
既然已經有了 DiffUtil.Callback 的實現之后,我們就需要對切換數據集的點擊事件進行處理了。
3、實現效果
關鍵代碼已經貼出來了,其實非常的簡單,最終運行的效果如下:
五、DiffUtil 效率問題
既然 DiffUtil 非常的好用,並且內部也實現了一套算法,但是我們也需要關心它的效率問題。
根據 Google 官方文檔中給出的例子,在 Nexus 5X M 系統上,DiffUtil 的效率問題,給出了一些參考的數據:
可以看到,實際上,DiffUtil 的算法把效率問題解決的非常的好。在開啟計算移動的情況下,1000 條數據中有 200 個修改,平均值也只有 13.54 ms ,基本上都是毫秒級的。
Google 官方同時也指出,如果是對大數據集的比對,最好是方在子線程中去完成計算,也就是其實是存在堵塞 UI 的情況的。所以如果你遇見了使用 DiffUtil 之后,每次刷新有卡頓的情況,可以考慮是否數據集太大,是否應該在子線程中完成計算。
六、結語
DiffUtil 已經介紹完了,如果覺得本文對你有幫助。都看到這里了,點個贊再走吧。