RecyclerView数据更新神器 - DiffUtil

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/IO_Field/article/details/79795584

概述

DiffUtil是support-v7:24.2.0新增的工具类,它主要是用来计算两个数据集之间的差异,计算出旧数据集->新数据集的最小变化量,并将其返回。

算法

DiffUtil内部采用ugene W. Myers’s difference 算法。该算法对空间做了优化,并使用O(N)空间来计算两个列表添加和删除的最小操作数,算法的时间复杂度为O(N + D ^ 2)。由于该算法不支持移动的Item,因而Google大牛在此基础上改进支持计算移动的Item。造成的后果就是,DiffUtil需要对结果进行第二遍运算,以便于计算移动的Item,从而更加耗费性能。此时,时间的复杂度为O(N ^ 2), 其中N是添加和删除操作的总数。对于根据约束条件排序的数据集,可以禁用移动Item的检测以提高性能。

用途

DiffUtil主要是与RecyclerView配合使用。其中,由DiffUtil找出每个Item的变化,由RecyclerView.Adapter更新UI。这样的好处就是,在数据集变化时,RecyclerView.Adapter不用无脑的调用notifyDataSetChanged()方法。

核心类

DiffUtil.Callback

DiffUtil.Callback是一个抽象类,在计算两个列表之间的差异时,由DiffUtil回调此类。在该类中,定义了5个抽象方法:

  • int getOldListSize(): 获取旧数据集的长度
  • int getNewListSize(): 获取新数据集的长度
  • boolean areItemsTheSame(int oldItemPosition, int newItemPosition):用来判断 两个对象是否是相同的Item
  • boolean areContentsTheSame(int oldItemPosition, int newItemPosition):用来检查 两个item是否含有相同的数据
  • Object getChangePayload(int oldItemPosition, int newItemPosition):后续再说

DiffUtil.DiffResult

DiffUtil.DiffResult用于保存DiffUtil计算出的数据集之间的差异信息,其可以将差异信息分配给RecyclerView.Adapter,以便更新UI。

核心方法

  • calculateDiff(DiffUtil.Callback cb)
  • calculateDiff(DiffUtil.Callback cb, boolean detectMoves)

这两个方法都是用来计算旧数据集->新数据集的最小变化量,并起将其返回。其中,第一个方法是第二个方法的特例,默认开启移动Item的检测:

public static DiffResult calculateDiff(Callback cb) {

    return calculateDiff(cb, true);

}

如果禁用移动Item的检测,可以调用第二个方法,并将detectMoves参数设置为false。

简单使用

前文,已经提到DiffUtil主要是与RecyclerView配合使用,以便高效的更新数据集。

  1. 创建Bean

    data class DiffBean(var name: String, var desc: String) {
    
        override fun equals(o: Any?): Boolean {
            if (this === o) return true
            if (o == null || javaClass != o.javaClass) return false
    
            val diff = o as DiffBean?
    
            return diff!!.name == name
        }
    
        override fun hashCode(): Int {
            var result = name?.hashCode() ?: 0
            return result
        }
    }
    
  2. 创建DiffUtil.Callback

    class DiffCallback(private val oldList: List<DiffBean>, private val newList: List<DiffBean>) : DiffUtil.Callback() {
        /**
         * 被DiffUtil调用,用来判断 两个对象是否是相同的Item。
         * 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等,或者重写equals方法
         */
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldList[oldItemPosition].name == newList[newItemPosition].name
        }
    
        /**
         * 老数据集size
         */
        override fun getOldListSize(): Int {
            return oldList.size
        }
    
        /**
         * 新数据集size
         */
        override fun getNewListSize(): Int {
            return newList.size
        }
    
        /**
         * 被DiffUtil调用,用来检查 两个item是否含有相同的数据
         * DiffUtil用返回的信息(true false)来检测当前item的内容是否发生了变化
         */
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldBean = oldList[oldItemPosition]
            val newBean = newList[newItemPosition]
    
            // 如果有内容不相同就返回false
    //            if (oldBean.name != newBean.name) {
    //                return false
    //            }
    
            if (!TextUtils.equals(oldBean.desc, newBean.desc)) {
                return false
            }
    
            //  //默认两个data内容是相同的
            return true
        }
    }
    

    在自定义的DiffCallback中,尤其要注意这两个方法:

    • areItemsTheSame()方法用来判断Item是否相同。如果Item有唯一的字段,即主键,可以判断两个主键是否相等,就像例子中 name字段作为主键,这么做: oldList[oldItemPosition].name == newList[newItemPosition].name。当然,也可以重写equals方法(),即 TextUtils.equals(oldList[oldItemPosition], newList[newItemPosition]).其核心点就是 当两个Item相同时,返回true,否则 返回false。
    • areContentsTheSame()用来判断Item是否相同的内容。如果Item的字段非常多,是否需要全部都需要比较呢?个人觉得,只需要对UI显示有影响的字段做比较即可,并不需要所有的字段都做出判断,不仅影响效率,也没啥用。
  3. 创建Apdater

    class DiffAdapter : RecyclerView.Adapter<DiffAdapter.ViewHolder>() {
        val mList: MutableList<DiffBean> = mutableListOf()
    
        ***         
    
        ***
    
        fun setData(list: List<DiffBean>) {
    
            //利用DiffUtil.calculateDiff()方法,传入一个规则DiffUtil.Callback对象,和是否检测移动item的 boolean变量,得到DiffUtil.DiffResult 的对象
            val result: DiffUtil.DiffResult = DiffUtil.calculateDiff(DiffCallback(mList, list), true)
    
            //利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年
            result.dispatchUpdatesTo(this)
            // 更新数据集,必须放在dispatchUpdatesTo之后,否则getChangePayload()将无效
            // 因为在getChangePayload()还需要对新旧数据集中的Item比较
            mList.clear()
            mList.addAll(list)
        }
    
        *** 
    }
    

    在setData()方法中,DiffUtil在调用calculateDiff()计算新旧数据集差异时,传递了两个参数,第一个参数为DiffUtil.Callback对象,第二个参数用来设置在计算时是否禁用检测移动的Item。当改为false时,将禁用检测移动的Item,此时效率更高。如果数据集已经根据给定条件进行排序,第二个参数可以设置为false,以提高计算的效率。

  4. 更新数据集

    mList.apply {
        add(DiffBean("A", "这是A"))
        add(DiffBean("B", "这是B"))
        add(DiffBean("C", "这是C"))
        add(DiffBean("D", "这是D"))
        add(DiffBean("E", "这是E"))
    }
    mAdapter.setData(mList)
    

可以看来,当使用DiffUtill和RecyclerView使用,再也不用无脑的调用notifyDataSetChanged()方法来更新UI。而,所看不见的是,更新UI的效率更高了,那就是源自DiffUtil内部的ugene W. Myers’s difference 算法。

UI是如何更新的?

当数据集更新时,RecyclerView.Adapter并没有调用notifyDataSetChanged()方法, UI确更新了?这里有点疑惑。在使用DiffUtil的过程中,与RecyclerView.Adapter有交集的只有DiffUtil.DiffResult的dispatchUpdatesTo()方法。这个方法是用来将DiffUtil计算出的由旧数据集->新数据集的最小量分配给RecyclerView.Adapter。接下来,跟踪下它的源码:

public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
    dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}

在dispatchUpdatesTo(RecyclerView.Adapter)方法中又调用了dispatchUpdatesTo(ListUpdateCallback)方法,此时,将我们传递过去的Adapter创建了AdapterListUpdateCallback对象:

public final class AdapterListUpdateCallback implements ListUpdateCallback {

    private final RecyclerView.Adapter mAdapter;


    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

AdapterListUpdateCallback是ListUpdateCallback的实现类,其内调用了Adapter一些列更新UI的方法。而在dispatchUpdatesTo(ListUpdateCallback)方法中,根据更新操作,即旧数据集->新数据集的操作,分配给指定的回调,从而更新UI。

这也就是说,DiffUtil不仅可以跟RecyclerView使用,还可以与ListView,或者是其他的列表,一起使用。我们只需自定义ListUpdateCallback,实现它的4个方法,然后调用dispatchUpdatesTo(ListUpdateCallback)方法,将更新操作分发出去即可。至于更新操作分发的细节,有兴趣的可以查看dispatchUpdatesTo(ListUpdateCallback)方法的源码。

getChangePayload

暂且不谈getChangePayload()方法,先来看RecyclerView中的一个方法:

public void onBindViewHolder(@NonNull VH holder, int position,
        @NonNull List<Object> payloads) {
    onBindViewHolder(holder, position);
}

对于该方法,官方是这么介绍的:

由RecyclerView调用以在指定位置显示数据. 此方法 更新ViewHolder的itemView的内容以反映给指定位置的Item的变化。

请注意: 与ListView不同的是,如果给定位置的item的数据集变化了,RecyclerView不会再次调用这个方法,除非item本身失效了invalidated ) 或者新的位置不能确定。 由于这个原因,在这个方法里,你应该只使用 postion参数 去获取相关的数据item,而且不应该>去保持 这个数据item的副本。如果稍后需要Item的 postion,比如,在点击事件监听中,使用 ViewHolder.getAdapterPosition(),它>能提供 更新后的位置。

部分绑定 vs完整绑定

payloads 参数 是一个从(notifyItemChanged(int, Object)或notifyItemRangeChanged(int, int, Object))里得到的合并list。 如果payloads list不为空,ViewHolder当前与旧数据绑定,而Adapter可以使用payload 信息运行高效的局部更新

如果payload为null,Adapter必须执行完整绑定。Adapter不应该认为onBindViewHolder()会接收到notify方法传递的有效信息。例如,当View没有attached 在屏幕上时,这个来自notifyItemChange()的payload 就简单的丢掉好了。

到这里可以明白,Adapter调用这个方法可以根据指定位置Item的变化以高效地执行局部更新。接下来,在来看getChangePayload()方法:

@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
    return null;
}

官方文档是这么介绍的:

areItemsTheSame(int, int)返回true并且areContentsTheSame(int, int)方法返回false时,DiffUtil将调用此方法,获取内容变化的payload。
例如,如果将DiffUtil与RecyclerView一起使用,则可以返回Item更改的特定的字段以及RecyclerView.ItemAnimator ItemAnimator可以使用这些信息运行正确的动画。

默认返回null

简单的来说,getChangePayload()方法返回的Object对象,其包括Item更改的内容,或者其他UI更新更新相关的信息,比如RecyclerView.ItemAnimator。

也就是说,当DiffUtil与RecyclerView一起使用时,如果Adapter想执行高效地局部更行,首先应重写DiffUtil.Callback的getChangePayload()方法,并将指定位置Item的变化信息返回:

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
    val oldBean = oldList[oldItemPosition]
    val newBean = newList[newItemPosition]

    val bundle = Bundle()

    if (!TextUtils.equals(oldBean.desc, newBean.desc)) {
        bundle.putString("desc", "getChangePayLoad: " + newBean.desc)
    } else { // 如果没有数据变化,返回null
        return null
    }

    return bundle
}

然后,在自定义的RecyclerView.Adapter中重写onBindViewHolder(@NonNull VH holder, int position,@NonNull List<Object> payloads)方法,以便在Item更新内容时,Adapter是执行局部绑定还是完整绑定:

override fun onBindViewHolder(holder: DiffViewHolder, position: Int, payloads: MutableList<Any>) {
    // 如果payload为null,Adapter必须运行完整绑定
    if (payloads.isEmpty()) {
        onBindViewHolder(holder, position)
    } else {
        val bundle = payloads[0] as Bundle
        holder.tvDesc.text = bundle.getString(KEY_DESC)
    }
}

当payloads为空时,Adapter必须执行完整绑定。至于原因看上文。

总结

  1. DiffUtil不仅可以配合RecyclerView,还可以与ListView等其他List配合使用,只需要自定义ListUpdateCallback即可,也就是更新操作的实现。
  2. 由于更新UI要在主线程,而DiffUtil又是耗时操作,当数据量大时,DiffUtil耗时也是漫长的,如果在主线程调用DiffUtil.calculateDiff,可能造成ANR。所以,应当开启线程执行DiffUtil.calculateDiff而在主线程调用result.dispatchUpdatesTo(this)分发更新操作。可以这么做:

    • 线程+Handler
    • RxJava

对于DiffUtil不能在主线程计算差异的问题,还可以使用DiffUtil的封装类AsyncListDiffer或者ListAdapter。
3. RecyclerView.Adapter更新数据集必须放在分配更新操作之后,也就是DiffResutl调用dispatchUpdatesTo()以后。因为在此之前更新数据集,getChangePayload()将无效,因为在getChangePayload()还需要对新旧数据集中的Item比较。

Demo地址

DiffUtilDemo

参考文档

  1. DiffUtil
  2. 【Android】详解7.0带来的新工具类:DiffUtil
  3. Actor

猜你喜欢

转载自blog.csdn.net/IO_Field/article/details/79795584