使用DiffUtil高效更新RecyclerView

来源:互联网 发布:海尔智能电视软件 编辑:程序博客网 时间:2024/06/06 14:19

本文转自: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。

    @Overridepublic 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 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 膝关节韧带拉伤怎么办恢复快 脚踝韧带拉伤怎么办恢复快 脚扭伤一年没好怎么办 脚扭伤半年还疼怎么办 脚崴过有后遗症怎么办 脚扭伤脚面肿了怎么办 腰突然扭了好痛怎么办 腰扭伤了怎么办最有效 腰扭伤了不能动怎么办 前交叉韧带增粗怎么办 膝盖前交叉韧带损伤怎么办 狗的腿肌肉拉伤怎么办 胳膊上的筋拉伤怎么办 肩周炎胳膊抬不起来怎么办 脖子上的筋拉伤怎么办 脚踝骨扭伤肿了怎么办 脚扭伤肿起来了怎么办 月经量特别少该怎么办 月经血沾床单上怎么办 月经弄到棉被上怎么办 血弄床单上干了怎么办 不小心吃了指甲怎么办 月经没有干净同房了怎么办 撞红了怎么办要吃药吗 自己长得太丑怎么办 长得丑特别自卑怎么办 手挤了有淤血怎么办 手指肚夹淤血了怎么办 指甲被夹了变黑怎么办 孩子手指夹肿了怎么办 指甲压了有淤血怎么办 挤到手指有淤血怎么办 指甲上有黑线是怎么办 指甲被压了要掉怎么办 脚趾肿了有脓怎么办 手指甲上的死皮怎么办 手被东西砸肿了怎么办 手被夹住有淤血怎么办 指头被车门夹了怎么办 手背夹伤了肿了怎么办 剪完指甲边肿了怎么办