目录

  • 场景介绍
  • DiffUtil
    • 怎样用
    • 代码剖析
  • ReyclerView.ListAdapter
    • 仍是先来看看怎样用
    • 源码剖析

场景介绍

先介绍一下MVVM

我相信咱们都现已十分熟悉了

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

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")

作用大概是这样的

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

咱们能够看到,两个不同的数据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

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

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。