本文转载自作者: 承香墨影,附上作者微信和公众号
一、前言
DIffUtils 是 Support-v7:24:2.0 中,更新的工具类。因为已经更新了一段时间了,也不好说是最新更新的。
它主要是为了配合RecyclerView 使用,通过比对新、旧两个数据集的差异,生成旧数据到新数据的最小变动,然后对有变动的数据项,进行局部刷新。
接下来就 DiffUtil 的使用细节,进行一个详细的讲解,希望一篇文章就完全理解 DiffUtil。
二、为什么会有DiffUtil
RecyclerView 自从被发布以来,一直被说成是 ListView、GridView 等一系列列表控件的完美替代品。并且它本身使用起来也非常的好用,布局切换方便、自带ViewHolder、局部更新并且可带更新动画等等。
局部更新、并且可以很方便的设置更新动画这一点,是 RecyclerView 一个不错的亮点。它为此提供了对应的方法:
adapter.notifyItemChange()
adapter.notifyItemInserted()
adapter.notifyItemRemoved()
adapter.notifyItemMoved();
以上方法都是为了对数据集中,单一项进行操作,并且为了操作连续的数据集的变动,还提供了对应的 notifyRangeXxx() 方法。
虽然 RecyclerView 提供的局部更新的方法,看似非常的好用,但是实际上,其实并没有什么用。
在实际开发中,最方便的做法就是无脑调用 notifyDataSetChanged(),用于更新 adapter 的数据集。
虽然 notifyDataSetChanged 有一些缺点:
不会触发 RecyclerView 的局部更新的动画。
性能低,会刷新整个 RecyclerView 可视区域。
但是真有需要频繁刷新,前后两个数据集的场景,一个 notifyDataSetChanged() 方法,会比自己写一个数据集比对方法,然后去计算他们的差值,最后调用对应的方法更新到 RecyclerView 中去。
我这么懒,如果不是必要,当然是会选 notifyDataSetChanged() 了。毕竟和之前 ListView 的时候,也没有更差了。
Google 显然也发现了这个问题,所以 DiffUtil 被发布了。
三、介绍DiffUtil
就像前面说的,DiffUtil 就是为了解决这个痛点的。它能很方便的对两个数据集之间进行比对,然后计算出变动情况,配合RecyclerView.Adapter ,可以自动根据变动情况,调用 adapter 的对应方法。
当然,DiffUtil 不仅只能配合 RecyclerView 使用,它实际上可以单独用于比对两个数据集,然后如何操作是可以定制的,那么在什么场景下使用,就全凭我们自己发挥了。
DiffUtil 在使用起来,主要需要关注几个类:
DiffUtil.Callback:具体用于限定数据集比对规则。
DiffUtil.DiffResult:比对数据集之后,返回的差异结果。
1、DiffUtil.Callback
DiffUtil.Callback 主要就是为了限定两个数据集中,子项的比对规则。毕竟开发者面对的数据结构多种多样,既然没法做一套通用的内容比对方式,那么就将比对的规则,交还给开发者来实现即可。
在 Callback 中,其实只需要实现 4 个方法:
getOldListSize():旧数据集的长度。
getNewListSize():新数据集的长度
areItemsTheSame():判断是否是同一个Item。
areContentsTheSame():如果是通一个Item,此方法用于判断是否同一个 Item 的内容也相同。
前两个是获取数据集长度的方法,这没什么好说的。但是后两个方法,主要是为了对应多布局的情况产生的,也就是存在多个 viewType 和多个 ViewHodler 的情况。首先需要使用 areItemsTheSame() 方法比对是否是同一个 viewType(也就是同一个ViewHolder) ,然后再通过 areContentsTheSame() 方法比对其内容是否也相等。
其实 Callback 还有一个 getChangePayload() 的方法,它可以在 ViewType 相同,但是内容不相同的时候,用 payLoad 记录需要在这个 ViewHolder 中,具体需要更新的View。
areItemsTheSame()、areContentsTheSame()、getChangePayload() 分别代表了不同量级的刷新。
首先会通过 areItemsTheSame() 判断当前 position 下,ViewType是否一致,如果不一致就表明当前position下,从数据到UI结构上全部变化了,那么就不关心内容,直接更新就好了。如果一致的话,那么其实View是可以复用的,就还需要再通过 areContentsTheSame() 方法判断其内容是否一致,如果一致,则表示是同一条数据,不需要做额外的操作。但是一旦不一致,则还会调用 getChangePayload() 来标记到底是哪个地方的不一样,最终标记需要更新的地方,最终返回给 DiffResult 。
当然,对性能要是要求没那么高的情况下,是可以不使用 getChangedPayload() 方法的。
2、DiffUtil.DiffResult
DiffUtil.DiffResult 其实就是 DiffUtil 通过 DiffUtil.Callback 计算出来,两个数据集的差异。它是可以直接使用在 RecyclerView 上的。如果有必要,也是可以通过实现 ListUpdateCallback 接口,来比对这些差异的。
3、使用DiffUtil
介绍了 Callback 和 DiffResult 之后,其实就可以正常使用 DiffUtil 来进行数据集的比对了。
在这个过程中,其实其实很简单,只需要调用两个方法:
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(oldDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);
calculateDiff 方法主要是用于通过一个具体的 DiffUtils.Callback 实现对象,来计算出两个数据集差异的结果,得到 DiffUtil.DiffResult 。
而 calculateDiff 的另外一个参数,用于标记是否需要检测 Item 的移动。
DiffUtil 使用的是 Eugene Myers 的差别算法,这个算法本身是不检查元素的移动的。也就是说,有元素的移动它也只是会先标记为删除,然后再标记插入。而如果需要计算元素的移动,它实际上也是在通过 Eugene Myers 算法比对之后,再进行一次移动检查。所以,如果集合本身已经排序过了,可以不进行移动的检查。
而 dispatchUpdatesTo() 就是将这个数据集差异的结果,通过 adapter 更新到 RecyclerView 上面。
实际上 dispatchUpdatesTo(Adapter) ,也是使用的ListUpdateCallback 这个接口,在其中获得差异,然后调用 adapter 的对应方法。
四、上例子
既然已经说清楚了,那么我们开始上例子了。
功能很简单,有四个数据集,使用 RecyclerView 承载,然后有一个按钮,用于轮换的切换数据集。
1、实现 DiffUtil.Callback
为了简单,RecyclerView 中使用单一 ViewType ,并且使用一个TextView 承载一个 字符串来显示。
那么我们开始实现 Callback:
2、切换数据集
既然已经有了 DiffUtil.Callback 的实现之后,我们就需要对切换数据集的点击事件进行处理了。
3、实现效果
关键代码已经贴出来了,其实非常的简单,最终运行的效果如下:
五、DiffUtil 效率问题
既然DiffUtil 非常的好用,并且内部也实现了一套算法,但是我们也需要关心它的效率问题。
根据 Google 官方文档中给出的例子,在 Nexus 5X M 系统上,DiffUtil 的效率问题,给出了一些参考的数据:
可以看到,实际上,DiffUtil 的算法把效率问题解决的非常的好。在开启计算移动的情况下,1000 条数据中有 200 个修改,基本上平均值也是 在 13.54 ms ,基本上都是毫秒级的。
Google 官方同时也指出,如果是对大数据集的比对,最好是方在子线程中去完成计算,也就是其实是存在堵塞 UI 的情况的。所以如果你遇见了使用 DiffUtil 之后,每次刷新有卡顿的情况,可以考虑是否数据集太大,是否应该在子线程中完成计算。