使用 DiffUtil 高效更新 RecyclerView

3,385 阅读5分钟

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()方法判断是否这项数据是否被更新。

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

    class MyDiffCallback extends DiffUtil.Callback {
     private List oldList;
     private List newList;
    
    public MyDiffCallback(List oldList, List 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,具体使用什么方式来表示数据的更新并不重要,重要的是在这个方法中你把更新情况存入一个对象后,在后面还能从同一个对象中把更新的情况取出来。

    这就很简单了,我在收到新数据后新建了一个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();
    

    首先我在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。

    DiffUtil is a Must!

    DiffUtil Reference

    参考文献:

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

    结语

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List 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;
     }
     }
    }
    

    qr

    欢迎关注我的公众号,将零碎时间都用在刷干货上!