使用DiffUtil高效更新RecyclerView

本文转自:http://blog.chengdazhi.com/index.php/231?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

DiffUtil是recyclerview support library v7 24.2.0版本中新增的类,根据Google官方文档的介绍,DiffUtil的作用是比较两个数据列表并能计算出一系列将旧数据表转换成新数据表的操作。这个概念比较抽象,换一种方式理解,DiffUtil是一个工具类,当你的RecyclerView需要更新数据时,将新旧数据集传给它,它就能快速告知adapter有哪些数据需要更新。

那么相比直接调用adapter.notifyDataSetChange()方法,使用DiffUtil有什么优势呢?它能在收到数据集后,提高UI更新的效率,而且你也不需要自己对新老数据集进行比较了。

顾名思义,凡是数据集的比较DiffUtil都能做,所以用处并不止于更新RecyclerView。DiffUtil也提供了回调让你可以进行其他操作。本文只讨论使用DiffUtil更新RecyclerView。

DiffUtil简介

在使用DiffUtil前我们先简单看看DiffUtil的特性。DiffUtil使用Eugene W. Myers的Difference算法来计算出将一个数据集转换为另一个的最小更新量,也就是用最简单的方式将一个数据集转换为另一个。除此之外,DiffUtil还可以识别一项数据在数据集中的移动。Eugene的算法对控件进行了优化,在查找两个数据集间最少加减操作时的空间复杂度为O(N),时间复杂度为O(N+D^2)。而如果添加了对数据条目移动的识别,复杂度就会提高到O(N^2)。所以如果数据集中数据不存在移位情况,你可以关闭移动识别功能来提高性能。

当然这些算法都是封装好的,使用时并不需要关注。下面是谷歌官网给出的在Nexus 5X M系统上进行运算的时长:

  • 100项数据,10处改动:平均值0.39ms,中位数:0.35ms。
  • 100项数据,100处改动:
    1. 打开了移位识别时:平均值:3.82ms,中位数:3.75ms。
    2. 关闭了移位识别时:平均值:2.09ms,中位数:2.06ms。
  • 1000项数据,50处改动:
    1. 打开了移位识别时:平均值:4.67ms,中位数:4.59ms。
    2. 关闭了移位识别时:平均值:3.59ms,中位数:3.50ms。
  • 1000项数据,200处改动:
    1. 打开了移位识别时:平均值:27.07ms,中位数:26.92ms。
    2. 关闭了移位识别时:平均值:13.54ms,中位数:13.36ms。

当数据集较大时,你应该在后台线程计算数据集的更新。这一点在后面的代码中会再次说到。

使用方式

使用DiffUtil时涉及以下几个核心类:

  • DiffUtil.Callback:这是最核心的类,不要被命名困惑,它不像你日常所使用的回调。你可以将它理解成比较新老数据集时的规则
  • DiffUtil:通过静态方法DiffUtil.calculateDiff(DiffUtil.Callback)来计算数据集的更新。
  • DiffResult:是DiffUtil的计算结果对象,通过DiffResult.dispatchUpdatesTo(RecyclerView.Adapter)来进行更新。

所以使用步骤如下:

  1. 自定义类继承DiffUtil.Callback,通过覆盖特定方法给出数据比较逻辑
  2. 调用DiffUtil.calculateDiff(DiffUtil.Callback callback[, boolean detectMove])来计算更新,得到DiffResult对象。第二个参数可省,意为是否探测数据的移动,是否关闭需要根据数据集情况来权衡。当数据集很大时,此操作可能耗时较长,需要异步计算。
  3. 在UI线程中调用DiffResult.dispatchUpdatesTo(RecyclerView.Adapter),而后Adapter的onBindViewHolder(RecyclerView.ViewHolder holder, int position, Listpayloads)。注意这个方法比必须覆盖的onBindViewHolder(RecyclerView.ViewHolder holder, int position)方法多一个参数payloads,而里面存储了数据的更新。

    放码

    在这里我们使用的数据模式是Item,有四个属性,其中一个是id。我们在这里的逻辑是,根据id判断两个Item对象是不是一项数据,如果是一项数据,则根据Item.equals()方法判断是否这项数据是否被更新。

    下面按照上文给出的三个步骤,给出示例代码。

    1. 继承DiffUtil.Callback。
    class MyDiffCallback extends DiffUtil.Callback {
     private List<Item> oldList;
     private List<Item> newList;
    
    public MyDiffCallback(List<Item> oldList, List<Item> newList) {
     this.oldList = oldList;
     this.newList = newList;
     }
    
    @Override
     public int getOldListSize() {
     return oldList.size();
     }
    
    @Override
     public int getNewListSize() {
     return newList.size();
     }
    
    @Override
     public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
     return oldList.get(oldItemPosition).id == newList.get(newItemPosition).id;
     }
    
    @Override
     public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
     return oldList.get(oldItemPosition).equals(newList.get(newItemPosition
     ));
     }
    
    @Nullable
     @Override
     public Object getChangePayload(int oldItemPosition, int newItemPosition) {
     Item oldItem = oldList.get(oldItemPosition);
     Item newItem = newList.get(newItemPosition);
     Bundle diffBundle = new Bundle();
     if (!newItem.title.equals(oldItem.title)) {
     diffBundle.putString(KEY_TITLE, newItem.title);
     }
     if (!newItem.content.equals(oldItem.content)) {
     diffBundle.putString(KEY_CONTENT, newItem.content);
     }
     if (!newItem.footer.equals(oldItem.footer)) {
     diffBundle.putString(KEY_FOOTER, newItem.footer);
     }
     if (diffBundle.size() == 0)
     return null;
     return diffBundle;
     }
    }
    

    除了最后一个getChangePayload()方法,其他都很好理解。最后一个方法的调用情况是:areItemsTheSame()返回true而areContentsTheSame()返回false,也就是说两个对象代表的数据是一条,但是内容更新了。在getChangePayload()方法中,你要给出具体的变化。这里我使用的Bundle,具体使用什么方式来表示数据的更新并不重要,重要的是在这个方法中你把更新情况存入一个对象后,在后面还能从同一个对象中把更新的情况取出来。

    1. 计算数据更新情况

    这就很简单了,我在收到新数据后新建了一个Runnable来计算,并把得到的DiffResult对象发送到Handler。

    new Thread(new Runnable() {
     @Override
     public void run() {
     DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(oldData, data));
     Message message = handler.obtainMessage();
     message.obj = diffResult;
     handler.dispatchMessage(message);
     }
    }).start();
    
    1. 将结果发送给Adapter并更新UI

    首先我在handler中将数据发送给adapter:

    if (msg.obj instanceof DiffUtil.DiffResult) {
     runOnUiThread(new Runnable() {
     @Override
     public void run() {
     ((DiffUtil.DiffResult) msg.obj).dispatchUpdatesTo(adapter);
     }
     });
    }
    

    然后重写RecylerView.Adapter.onBindViewHolder(RecyclerView.ViewHolder holder, int position, Listpayloads)方法,通过payloads.get(0)获取到在DiffUtil.Callback.getChangePayload()方法中返回的Bundle,并取出数据更新情况以更新UI。

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List<Object> payloads) {
     if (holder instanceof MyViewHolder) {
     ((MyViewHolder) holder).bindData(data.get(position));
     }
     if (payloads == null || payloads.isEmpty()) {
     return;
     }
     Bundle o = (Bundle) payloads.get(0);
     for (String key : o.keySet()) {
     switch (key) {
     case KEY_TITLE:
     ((MyViewHolder) holder).updateTitle(o.getString(KEY_TITLE));
     break;
     case KEY_CONTENT:
     ((MyViewHolder) holder).updateContent(o.getString(KEY_CONTENT));
     break;
     case KEY_FOOTER:
     ((MyViewHolder) holder).updateFooter(o.getString(KEY_FOOTER));
     break;
     }
     }
    }
    

    结语

    DiffUtil可用于高效进行RecyclerView的数据更新,但DiffUtil本身的作用是计算数据集的最小更新。DiffUtil有强大的算法支撑,可以利用DiffUtil完成许多其他功能。

    参考文献:

    DiffUtil Reference

    DiffUtil is a Must!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Android 中,DiffUtil 是一个非常有用的工具类,可以帮助我们避免整个 RecyclerView 的刷新,只需要刷新发生变化的部分即可。下面介绍一下如何在低版本的 Android 系统中使用 DiffUtil: 1. 添加依赖库 在项目的 build.gradle 文件中,添加以下依赖库: ``` dependencies { implementation "androidx.recyclerview:recyclerview:版本号" } ``` 2. 创建 DiffUtil.Callback 实现类 创建一个实现 DiffUtil.Callback 接口的类,该接口有四个方法需要实现: - getOldListSize():返回旧数据集合的大小。 - getNewListSize():返回新数据集合的大小。 - areItemsTheSame():判断两个数据是否是同一个对象。 - areContentsTheSame():判断两个数据内容是否相同。 例如: ``` class MyDiffCallback(private val oldList: List<String>, private val newList: List<String>) : DiffUtil.Callback() { override fun getOldListSize(): Int { return oldList.size } override fun getNewListSize(): Int { return newList.size } override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } } ``` 3. 调用 DiffUtil.calculateDiff() 方法 在需要更新数据的地方,创建一个 DiffUtil.Callback 实例,并调用 DiffUtil.calculateDiff() 方法,该方法会返回一个 DiffUtil.DiffResult 实例,我们可以在该实例的 dispatchUpdatesTo() 方法中调用 RecyclerView.Adapter.notifyItemRangeChanged() 方法来更新 RecyclerView 中的数据。 例如: ``` val diffCallback = MyDiffCallback(oldList, newList) val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(adapter) ``` 通过以上步骤,我们就可以在低版本的 Android 系统中使用 DiffUtil 来避免整个 RecyclerView 的刷新,只需要刷新发生变化的部分即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值