【RecyclerView】别再notifyDataSetChanged()了!使用DiffUtil让你的RecyclerView更加丝滑

4,554 阅读7分钟

场景介绍

先介绍一下MVVM

我相信大家都已经非常熟悉了

8349937-acf4b3b5c9318cbd.webp

ViewModel负责提供数据给View,通常我们会观察ViewModel的数据变化,当接收到了新数据后就刷新UI。

通常可能我们会这么写

//在View层观察数据
 mViewModel.observeData().observe(activity, Observer { data->  
    //提交数据给adapter 更新UI                                                  
    adapter.submitList(data)                                                           
 })                                                                      

//在Adapter中这么写
var mList: List<String>? = null
fun submitList(list: List<String>) {
    mList = list
    notifyDataSetChanged()
}

假设data1 和data2是这样的

val data1 = listOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "S", "Y", "Z")

val data2 = listOf( "K", "A", "b", "C", "Z", "E", "F", "G", "H", "I", "J","L", "m","N","P","P","Q","S","T","U","V","W","S","Y","A","C")

效果大概是这样的

我们可以看到,两个不同的数据data1,data2 有数据的移动,变化(大小写改变为变化),增加,删除

但是这些变化并没有很明显的展示出来,只是很生硬的一个视图的变化。

如果我们想让每个元素的增删改移都有对应的动画的话,怎么办呢?

大概是这样的一个效果

我们知道,要有增删改移的动画效果,需要我们调用RecyelerView.Adapter

public final void notifyItemInserted(int position)
  
public final void notifyItemRemoved(int position)
  
public final void notifyItemChanged(int position)
  
public final void notifyItemMoved(int fromPosition, int toPosition)

那么问题就转化为,怎么快速的从data1data2中找出哪个元素增加了、删除了、修改了、移动了

大家可以花一点时间思考一下这个问题,这个其实是一个非常复杂的差分问题

但!庆幸的是,Google官方为我们提供了DiffUtil 可以帮助我们进行这个操作

接下来介绍DiffUtil

DiffUtil

这个工具类是基于Eugene W. Myers的一篇论文,于1986年11月发表在“Algorithmica”杂志上

An O(Nd) difference Algorithm and Its Variations

这是一个时间复杂度 O(N + D^2) 的算法,D是两个数据直接修改的大小,

在实际数据修改当中,D应该不大,所以这个效率算是非常高的了。

(Myers差分算法没有实现元素移动的操作,所以这里而外的使用了 O(N^2) 的时间去寻找移动元素 )

怎么用

在我们上面举的例子里面的代码这么修改就可以了

//在View层观察数据
 mViewModel.observeData().observe(activity, Observer { data->                                           
            val oldList = adapter.mList ?: listOf()
            val newList = data
            adapter.submitList(newList)
            DiffUtil.calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize() = oldList.size

                override fun getNewListSize() = newList.size

                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    //不考虑大小写,如果equals就是同个元素
                    return oldList[oldItemPosition].equals(newList[newItemPosition],true)
                }

                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                   //如果equals就内容相同
                    return oldList[oldItemPosition] == newList[newItemPosition]
                }
            }, false).dispatchUpdatesTo(adapter)                                                         
 })                                                                      

//在Adapter中这么写
var mList: List<String>? = null
fun submitList(list: List<String>) {
    mList = list
}

代码分析

先看看calculateDiff这个函数

/**                                                                                                      
 *                                                                                            
 * @param cb 这个回调主要会提供一些函数让查分算法可以进行判断,从此可以得出对比结果                                
 * @param detectMoves 需不需要检测数据源的移动,上面提到,如果需要检测会花额外O(N^2)的时间,N是删除和增加元素的和      
 *                                                                                                       
 * @return 数据比对的结果,我们可以用这个结果来更新UI                                                                  
 */                                                                                                      
public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) 

然后在看看第一个入参

public abstract static class Callback {
    /**
     * @return 原始数据的大小
     */
    public abstract int getOldListSize();

    /**
     * @return 新数据的大小
     */
    public abstract int getNewListSize();

    /**
     * @param oldItemPosition 原始数据索引
     * @param newItemPosition 新数据索引
     * @return 原始数据和新数据是不是同一个元素,通常我们认为ID相等就是同一个元素
     */
    public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

    /**
     * @param oldItemPosition 原始数据索引
     * @param newItemPosition 新数据索引
     * @return 相同的元素是否内容一样,内容不一样的话会刷新,通常我们认为元素的值(equals)一样就说明内容相同,
     * 当然这个equals要自己实现
     */
    public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

}

然后看看calculateDiff这个函数的返回值DiffResult能干什么

看源码能发现里面保存着Myers差分算法后的各种数据,

通常我们只需要使用里面封装好的这两个函数

 //1
 public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
     dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
 }
 //2
 public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {     

//1 就是解析校验结果后自动的调用adapter的各种notify方法

//2 就是将解析的校验结果回调出来,一般我们使用//1 就可

细心的同学会发现,我们所有操作都是在主线程,Myers差分算法用了O(N + D^2) 判断元素移动用了 O(N^2)

如果匹配的元素多的话,一百个一千个的话 岂不是会影响应用的流畅性

是的 的确会

我们看DiffUtil 注释能看到这个匹配的效率

  • 100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
  • 100 items and 100 modifications: 3.82 ms, median: 3.75 ms
  • 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
  • 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
  • 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
  • 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
  • 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms

1000个元素200个修改 加上移动的情况下 耗时为27.07ms 差不多两帧了,在现在普遍120赫兹的手机上,可能会掉更多的帧率

(不过测试机器是7年前发布的 Nexus 5X,现在的机器应该都会比这个好很多吧 (手动狗头)

那我们是不是需要将这个对比移到子线程了?

诶!等等,可以不用,Google爸爸给我们封装好了一个ListAdapter,里面维护着线程池并且还会为我们将视图修改操作移到主线程,这样我们就可以很方便的使用DiffUtil

ReyclerView.ListAdapter

还是先来看看怎么用

把刚才的代码拿出来再改一改

//在View层观察数据
mViewModel.observeData().observe(activity, Observer { data-> 
    adapter.submitList(data) 
})

//把Adapter的这个删掉
var mList: List<String>? = null
fun submitList(list: List<String>) {
    mList = list
}

//将原先继承于RecyclerView.Adapter<VH>改成ListAdapter<T, VH> 并且传入一个DiffUtil.ItemCallback<T>给ListAdapter
// T 是列表的数据类型,我们这里是String。实际上可以是一个Model或者data类
class MyListAdapter : ListAdapter<String, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<String>() {
                                                                                    
    override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {                                
        return oldItem.equals(newItem,true)                   
    }                                                                                                        
                                                                                                             
    override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {                             
        return  oldItem == newItem                                                                            
    }                                                                                                        
                                                                                                             
}) {                                                                                                         
                                                                                                             
   ··········                                  
}                                                                                                            

在看看DiffUtil.ItemCallbackareItemsTheSameareContentsTheSame 和我们刚才分析DiffUtil.CallBack的一样,只是这次不用我们根据索引去找元素了,这次直接给我们元素,让我们去判断。

源码分析

//ListAdapter继承至RecyclerView.Adapter 里面维护了一个AsyncListDiffer
public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>extends RecyclerView.Adapter<VH> {
   final AsyncListDiffer<T> mDiffer;
//AsyncListDiffer维护了一个AsyncDifferConfig和一个主线程的执行器mMainThreadExecutor
public class AsyncListDiffer<T> {
    private final ListUpdateCallback mUpdateCallback;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final AsyncDifferConfig<T> mConfig;
    Executor mMainThreadExecutor;
//AsyncDifferConfig是ListAdapter初始化的时候传进来的,mBackgroundThreadExecutor默认是一个线程池,主要来做异步计算的
public final class AsyncDifferConfig<T> {
    @Nullable
    private final Executor mMainThreadExecutor;
    @NonNull
    private final Executor mBackgroundThreadExecutor;
    @NonNull
    private final DiffUtil.ItemCallback<T> mDiffCallback;

他们的主要职责是这样的

  • ListAdapter提交数据给AsyncListDiffer,计算出差分结果后进行notify
  • AsyncListDiffer根据提交的数据使用AsyncDifferConfig的线程池执行差分操作,然后使用mMainThreadExecutor回调更新给ListAdapter
  • AsyncDifferConfig 差分的子线程主线程配置,默认mBackgroundThreadExecutor是一个FixedThreadPool

我们看看ListAdapter.submitList(data)的一次总流程

##ListAdapter
//将数据交给AsyncListDiffer计算
public void submitList(@Nullable List<T> list) {
    mDiffer.submitList(list);
}

##AsyncListDiffer
//使用DiffUtil计算查分结果,将数据变跟通知ListAdapter设置的callback
public void submitList(@Nullable final List<T> newList,                                                           
        @Nullable final Runnable commitCallback) {     
        
        
   ······
   
    final List<T> oldList = mList;            
    //在子线程执行
    mConfig.getBackgroundThreadExecutor().execute(new Runnable() {                                                
        @Override                                                                                                 
        public void run() {  
        		//调用DiffUtil.calculateDiff 使用方法跟我们介绍DiffUtil的时候一样
            final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { 
            
              ·······                         
              
            });                                                                                                   
           //在主线程执行                                                                                                       
            mMainThreadExecutor.execute(new Runnable() {                                                          
                @Override                                                                                         
                public void run() {                                                                               
                    if (mMaxScheduledGeneration == runGeneration) {      
                    		//将result(DiffResult) 发送给ListAdpater设置的callback
                        latchList(newList, result, commitCallback);                                               
                    }                                                                                             
                }                                                                                                 
            });                                                                                                   
        }                                                                                                         
    });                                                                                                           
}


 void latchList(                                               
         @NonNull List<T> newList,                             
         @NonNull DiffUtil.DiffResult diffResult,              
         @Nullable Runnable commitCallback) {                 
         ·····
     //这里是不是很熟悉,之前讲过的,这个mUpdateCallback是ListAdapter初始化的时候创建的
     diffResult.dispatchUpdatesTo(mUpdateCallback);        
     ···
 }                                                             
                                                               


相信大家已经大概了解整个流程了吧?到这里,本文的讲解就差不多结束了。

具体更多关于DiffUtil实现的算法细节,可以参考以下文章(也是git diff指令的实现算法)

Myers‘Diff之贪婪算法

The Myers diff algorithm

码字不易 T.T

JBjg8.png