Android7.0 工具类:DiffUtil详解
一 概述
diffutil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。
说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,recyclerview。
就我使用的这几天来看,它最大的用处就是在recyclerview刷新时,不再无脑madapter.notifydatasetchanged()。
以前无脑madapter.notifydatasetchanged()有两个缺点:
1.不会触发recyclerview的动画(删除、新增、位移、change动画)
2.性能较低,毕竟是无脑的刷新了一遍整个recyclerview , 极端情况下:新老数据集一模一样,效率是最低的。
使用diffutil后,改为如下代码:
diffutil.diffresult diffresult = diffutil.calculatediff(new diffcallback(mdatas, newdatas), true);
diffresult.dispatchupdatesto(madapter);
它会自动计算新老数据集的差异,并根据差异情况,自动调用以下四个方法
adapter.notifyitemrangeinserted(position, count);
adapter.notifyitemrangeremoved(position, count);
adapter.notifyitemmoved(fromposition, toposition);
adapter.notifyitemrangechanged(position, count, payload);
显然,这个四个方法在执行时都是伴有recyclerview的动画的,且都是定向刷新方法,刷新效率蹭蹭的上升了。
老规矩,先上图,
图一是无脑madapter.notifydatasetchanged()的效果图,可以看到刷新交互很生硬,item突然的出现在某个位置:
图二是使用diffutils的效果图,最明显的是有插入、移动item的动画:
转成gif有些渣,下载文末demo运行效果更佳哦。
本文将包含且不仅包含以下内容:
1 先介绍diffutil的简单用法,实现刷新时的“增量更新”效果。(“增量更新”是我自己的叫法)
2 diffutil的高级用法,在某项item只有内容(data)变化,位置(position)未变化时,完成部分更新(官方称之为partial bind,部分绑定)。
3 了解到 recyclerview.adapter还有public void onbindviewholder(vh holder, int position, list<object> payloads)方法,并掌握它。
4 在子线程中计算diffresult,在主线程中刷新recyclerview。
5 少部分人不喜欢的notifyitemchanged()导致item白光一闪的动画 如何去除。
6 diffutil部分类、方法 官方注释的汉化
二 diffutil的简单用法
前文也提到,diffutil是帮助我们在刷新recyclerview时,计算新老数据集的差异,并自动调用recyclerview.adapter的刷新方法,以完成高效刷新并伴有item动画的效果。
那么我们在学习它之前要先做一些准备工作,先写一个普通青年版,无脑notifydatasetchanged()刷新的demo。
1 一个普通的javabean,但是实现了clone方法,仅用于写demo模拟刷新用,实际项目不需要,因为刷新时,数据都是从网络拉取的。:
class testbean implements cloneable { private string name; private string desc; ....//get set方法省略 //仅写demo 用 实现克隆方法 @override public testbean clone() throws clonenotsupportedexception { testbean bean = null; try { bean = (testbean) super.clone(); } catch (clonenotsupportedexception e) { e.printstacktrace(); } return bean; }
2 实现一个普普通通的recyclerview.adapter。
public class diffadapter extends recyclerview.adapter<diffadapter.diffvh> { private final static string tag = "zxt"; private list<testbean> mdatas; private context mcontext; private layoutinflater minflater; public diffadapter(context mcontext, list<testbean> mdatas) { this.mcontext = mcontext; this.mdatas = mdatas; minflater = layoutinflater.from(mcontext); } public void setdatas(list<testbean> mdatas) { this.mdatas = mdatas; } @override public diffvh oncreateviewholder(viewgroup parent, int viewtype) { return new diffvh(minflater.inflate(r.layout.item_diff, parent, false)); } @override public void onbindviewholder(final diffvh holder, final int position) { testbean bean = mdatas.get(position); holder.tv1.settext(bean.getname()); holder.tv2.settext(bean.getdesc()); holder.iv.setimageresource(bean.getpic()); } @override public int getitemcount() { return mdatas != null ? mdatas.size() : 0; } class diffvh extends recyclerview.viewholder { textview tv1, tv2; imageview iv; public diffvh(view itemview) { super(itemview); tv1 = (textview) itemview.findviewbyid(r.id.tv1); tv2 = (textview) itemview.findviewbyid(r.id.tv2); iv = (imageview) itemview.findviewbyid(r.id.iv); } } }
3 activity代码:
public class mainactivity extends appcompatactivity { private list<testbean> mdatas; private recyclerview mrv; private diffadapter madapter; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); initdata(); mrv = (recyclerview) findviewbyid(r.id.rv); mrv.setlayoutmanager(new linearlayoutmanager(this)); madapter = new diffadapter(this, mdatas); mrv.setadapter(madapter); } private void initdata() { mdatas = new arraylist<>(); mdatas.add(new testbean("张旭童1", "android", r.drawable.pic1)); mdatas.add(new testbean("张旭童2", "java", r.drawable.pic2)); mdatas.add(new testbean("张旭童3", "背锅", r.drawable.pic3)); mdatas.add(new testbean("张旭童4", "手撕产品", r.drawable.pic4)); mdatas.add(new testbean("张旭童5", "手撕测试", r.drawable.pic5)); } /** * 模拟刷新操作 * * @param view */ public void onrefresh(view view) { try { list<testbean> newdatas = new arraylist<>(); for (testbean bean : mdatas) { newdatas.add(bean.clone());//clone一遍旧数据 ,模拟刷新操作 } newdatas.add(new testbean("赵子龙", "帅", r.drawable.pic6));//模拟新增数据 newdatas.get(0).setdesc("android+"); newdatas.get(0).setpic(r.drawable.pic7);//模拟修改数据 testbean testbean = newdatas.get(1);//模拟数据位移 newdatas.remove(testbean); newdatas.add(testbean); //别忘了将新数据给adapter mdatas = newdatas; madapter.setdatas(mdatas); madapter.notifydatasetchanged();//以前我们大多数情况下只能这样 } catch (clonenotsupportedexception e) { e.printstacktrace(); } } }
很简单,只不过在构建新数据源newdatas时,是遍历老数据源mdatas,调用每个data的clone()方法,确保新老数据源虽然数据一致,但是内存地址(指针不一致),这样在后面修改newdatas里的值时,不会牵连mdatas里的值被一起改了。
4 activity_main.xml 删掉了一些宽高代码,就是一个recyclerview和一个button用于模拟刷新。:
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" > <android.support.v7.widget.recyclerview android:id="@+id/rv" /> <button android:id="@+id/btnrefresh" android:layout_alignparentright="true" android:onclick="onrefresh" android:text="模拟刷新" /> </relativelayout>
以上是一个普通青年很容易写出的,无脑notifydatasetchanged()的demo,运行效果如第一节图一。
但是我们都要争做文艺青年,so
下面开始进入正题,简单使用diffutil,我们需要且仅需要额外编写一个类。
想成为文艺青年,我们需要实现一个继承自diffutil.callback的类,实现它的四个abstract方法。
虽然这个类叫callback,但是把它理解成:定义了一些用来比较新老item是否相等的契约(contract)、规则(rule)的类, 更合适。
diffutil.callback抽象类如下:
public abstract static class callback { public abstract int getoldlistsize();//老数据集size public abstract int getnewlistsize();//新数据集size public abstract boolean areitemsthesame(int olditemposition, int newitemposition);//新老数据集在同一个postion的item是否是一个对象?(可能内容不同,如果这里返回true,会调用下面的方法) public abstract boolean arecontentsthesame(int olditemposition, int newitemposition);//这个方法仅仅是上面方法返回ture才会调用,我的理解是只有notifyitemrangechanged()才会调用,判断item的内容是否有变化 //该方法在diffutil高级用法中用到 ,暂且不提 @nullable public object getchangepayload(int olditemposition, int newitemposition) { return null; } }
本demo如下实现diffutil.callback,核心方法配有中英双语注释(说人话就是,翻译了官方的英文注释,方便大家更好理解)。
/** * 介绍:核心类 用来判断 新旧item是否相等 * 作者:zhangxutong * 邮箱:zhangxutong@imcoming.com * 时间: 2016/9/12. */ public class diffcallback extends diffutil.callback { private list<testbean> molddatas, mnewdatas;//看名字 public diffcallback(list<testbean> molddatas, list<testbean> mnewdatas) { this.molddatas = molddatas; this.mnewdatas = mnewdatas; } //老数据集size @override public int getoldlistsize() { return molddatas != null ? molddatas.size() : 0; } //新数据集size @override public int getnewlistsize() { return mnewdatas != null ? mnewdatas.size() : 0; } /** * called by the diffutil to decide whether two object represent the same item. * 被diffutil调用,用来判断 两个对象是否是相同的item。 * for example, if your items have unique ids, this method should check their id equality. * 例如,如果你的item有唯一的id字段,这个方法就 判断id是否相等。 * 本例判断name字段是否一致 * * @param olditemposition the position of the item in the old list * @param newitemposition the position of the item in the new list * @return true if the two items represent the same object or false if they are different. */ @override public boolean areitemsthesame(int olditemposition, int newitemposition) { return molddatas.get(olditemposition).getname().equals(mnewdatas.get(newitemposition).getname()); } /** * called by the diffutil when it wants to check whether two items have the same data. * 被diffutil调用,用来检查 两个item是否含有相同的数据 * diffutil uses this information to detect if the contents of an item has changed. * diffutil用返回的信息(true false)来检测当前item的内容是否发生了变化 * diffutil uses this method to check equality instead of {@link object#equals(object)} * diffutil 用这个方法替代equals方法去检查是否相等。 * so that you can change its behavior depending on your ui. * 所以你可以根据你的ui去改变它的返回值 * for example, if you are using diffutil with a * {@link android.support.v7.widget.recyclerview.adapter recyclerview.adapter}, you should * return whether the items‘ visual representations are the same. * 例如,如果你用recyclerview.adapter 配合diffutil使用,你需要返回item的视觉表现是否相同。 * this method is called only if {@link #areitemsthesame(int, int)} returns * {@code true} for these items. * 这个方法仅仅在areitemsthesame()返回true时,才调用。 * @param olditemposition the position of the item in the old list * @param newitemposition the position of the item in the new list which replaces the * olditem * @return true if the contents of the items are the same or false if they are different. */ @override public boolean arecontentsthesame(int olditemposition, int newitemposition) { testbean beanold = molddatas.get(olditemposition); testbean beannew = mnewdatas.get(newitemposition); if (!beanold.getdesc().equals(beannew.getdesc())) { return false;//如果有内容不同,就返回false } if (beanold.getpic() != beannew.getpic()) { return false;//如果有内容不同,就返回false } return true; //默认两个data内容是相同的 }
注释张写了这么详细的注释+简单的代码,相信一眼可懂。
然后在使用时,注释掉你以前写的notifydatasetchanged()方法吧,替换成以下代码:
//文艺青年新宠 //利用diffutil.calculatediff()方法,传入一个规则diffutil.callback对象,和是否检测移动item的 boolean变量,得到diffutil.diffresult 的对象 diffutil.diffresult diffresult = diffutil.calculatediff(new diffcallback(mdatas, newdatas), true); //利用diffutil.diffresult对象的dispatchupdatesto()方法,传入recyclerview的adapter,轻松成为文艺青年 diffresult.dispatchupdatesto(madapter); //别忘了将新数据给adapter mdatas = newdatas; madapter.setdatas(mdatas);
讲解:
步骤一
在将newdatas 设置给adapter之前,先调用diffutil.calculatediff()方法,计算出新老数据集转化的最小更新集,就是
diffutil.diffresult对象。
diffutil.calculatediff()方法定义如下:
第一个参数是diffutil.callback对象,
第二个参数代表是否检测item的移动,改为false算法效率更高,按需设置,我们这里是true。
public static diffresult calculatediff(callback cb, boolean detectmoves)
步骤二
然后利用diffutil.diffresult对象的dispatchupdatesto()方法,传入recyclerview的adapter,替代普通青年才用的madapter.notifydatasetchanged()方法。
查看源码可知,该方法内部,就是根据情况调用了adapter的四大定向刷新方法。
public void dispatchupdatesto(final recyclerview.adapter adapter) { dispatchupdatesto(new listupdatecallback() { @override public void oninserted(int position, int count) { adapter.notifyitemrangeinserted(position, count); } @override public void onremoved(int position, int count) { adapter.notifyitemrangeremoved(position, count); } @override public void onmoved(int fromposition, int toposition) { adapter.notifyitemmoved(fromposition, toposition); } @override public void onchanged(int position, int count, object payload) { adapter.notifyitemrangechanged(position, count, payload); } }); }
小结:
所以说,diffutil不仅仅只能和recyclerview配合,我们也可以自己实现listupdatecallback接口的四个方法去做一些事情。(我暂时不负责任随便一项想,想到可以配合自己项目里的九宫格控件?或者优化我上篇文章写的nestfulllistview?小安利,见 listview、recyclerview、scrollview里嵌套listview 相对优雅的解决方案:http://blog.csdn.net/zxt0601/article/details/52494665)
至此,我们已进化成文艺青年,运行效果和第一节图二基本一致,
唯一不同的是此时adapter.notifyitemrangechanged()会有item白光一闪的更新动画 (本文demo的postion为0的item)。 这个item一闪的动画有人喜欢有人恨,不过都不重要了,
因为当我们学会了第三节的diffutil搞基用法,你爱不爱这个itemchange动画,它都将随风而去。(不知道是不是官方bug)
效果就是第一节的图二,我们的item0其实图片和文字都变化了,但是这个改变并没有伴随任何动画。
让我们迈向 文艺青年中的文艺青年 之路。
三 diffutil的高级用法
理论:
高级用法只涉及到两个方法,
我们需要分别实现diffutil.callback的
public object getchangepayload(int olditemposition, int newitemposition)方法,
返回的object就是表示item改变了哪些内容。
再配合recyclerview.adapter的
public void onbindviewholder(vh holder, int position, list<object> payloads)方法,
完成定向刷新。(成为文青中的文青,文青青。)
敲黑板,这是一个新方法,注意它有三个参数,前两个我们熟,第三个参数就包含了我们在getchangepayload()返回的object。
好吧,那我们就先看看这个方法是何方神圣:
在v7-24.2.0的源码里,它长这个样子:
/** * called by recyclerview to display the data at the specified position. this method * should update the contents of the {@link viewholder#itemview} to reflect the item at * the given position. * <p> * note that unlike {@link android.widget.listview}, recyclerview will not call this method * again if the position of the item changes in the data set unless the item itself is * invalidated or the new position cannot be determined. for this reason, you should only * use the <code>position</code> parameter while acquiring the related data item inside * this method and should not keep a copy of it. if you need the position of an item later * on (e.g. in a click listener), use {@link viewholder#getadapterposition()} which will * have the updated adapter position. * <p> * partial bind vs full bind: * <p> * the payloads parameter is a merge list from {@link #notifyitemchanged(int, object)} or * {@link #notifyitemrangechanged(int, int, object)}. if the payloads list is not empty, * the viewholder is currently bound to old data and adapter may run an efficient partial * update using the payload info. if the payload is empty, adapter must run a full bind. * adapter should not assume that the payload passed in notify methods will be received by * onbindviewholder(). for example when the view is not attached to the screen, the * payload in notifyitemchange() will be simply dropped. * * @param holder the viewholder which should be updated to represent the contents of the * item at the given position in the data set. * @param position the position of the item within the adapter‘s data set. * @param payloads a non-null list of merged payloads. can be empty list if requires full * update. */ public void onbindviewholder(vh holder, int position, list<object> payloads) { onbindviewholder(holder, position); }
原来它内部就仅仅调用了两个参数的onbindviewholder(holder, position) ,(题外话,哎哟喂,我的nestfulllistview 的adapter也有几分神似这种写法,看来我离google大神又近了一步)
看到这我才明白,其实onbind的入口,就是这个方法,它才是和oncreateviewholder对应的方法,
源码往下翻几行可以看到有个public final void bindviewholder(vh holder, int position),它内部调用了三参的onbindviewholder。
关于recyclerview.adapter 也不是三言两句说的清楚的。(其实我只掌握到这里)
好了不再跑题,回到我们的三参数的onbindviewholder(vh holder, int position, list<object> payloads),这个方法头部有一大堆英文注释,我一直觉得阅读这些英文注释对理解方法很有用处,于是我翻译了一下,
翻译:
由recyclerview调用 用来在在指定的位置显示数据。
这个方法应该更新viewholder里的itemview的内容,以反映在给定的位置 item(的变化)。
请注意,不像listview,如果给定位置的item的数据集变化了,recyclerview不会再次调用这个方法,除非item本身失效了(invalidated ) 或者新的位置不能确定。
出于这个原因,在这个方法里,你应该只使用 postion参数 去获取相关的数据item,而且不应该去保持 这个数据item的副本。
如果你稍后需要这个item的position,例如设置clicklistener。应该使用 viewholder.getadapterposition(),它能提供 更新后的位置。
(二笔的我看到这里发现 这是在讲解两参的onbindviewholder方法
下面是这个三参方法的独特部分:)
**部分(partial)绑定**vs完整(full)绑定
payloads 参数 是一个从(notifyitemchanged(int, object)或notifyitemrangechanged(int, int, object))里得到的合并list。
如果payloads list 不为空,那么当前绑定了旧数据的viewholder 和adapter, 可以使用 payload的数据进行一次 高效的部分更新。
如果payload 是空的,adapter必须进行一次完整绑定(调用两参方法)。
adapter不应该假定(想当然的认为) 在那些notifyxxxx通知方法传递过来的payload, 一定会在 onbindviewholder()方法里收到。(这一句翻译不好 qaq 看举例就好)
举例来说,当view没有attached 在屏幕上时,这个来自notifyitemchange()的payload 就简单的丢掉好了。
payloads对象不会为null,但是它可能是空(empty),这时候需要完整绑定(所以我们在方法里只要判断isempty就好,不用重复判空)。
作者语:这方法是一个高效的方法。 我是个低效的翻译者,我看了40+分钟。才终于明白,重要的部分已经加粗显示。
实战:
说了这么多话,其实用起来超级简单:
先看如何使用getchangepayload()方法,又附带了中英双语注释
/** * when {@link #areitemsthesame(int, int)} returns {@code true} for two items and * {@link #arecontentsthesame(int, int)} returns false for them, diffutil * calls this method to get a payload about the change. * * 当{@link #areitemsthesame(int, int)} 返回true,且{@link #arecontentsthesame(int, int)} 返回false时,diffutils会回调此方法, * 去得到这个item(有哪些)改变的payload。 * * for example, if you are using diffutil with {@link recyclerview}, you can return the * particular field that changed in the item and your * {@link android.support.v7.widget.recyclerview.itemanimator itemanimator} can use that * information to run the correct animation. * * 例如,如果你用recyclerview配合diffutils,你可以返回 这个item改变的那些字段, * {@link android.support.v7.widget.recyclerview.itemanimator itemanimator} 可以用那些信息去执行正确的动画 * * default implementation returns {@code null}. * 默认的实现是返回null * * @param olditemposition the position of the item in the old list * @param newitemposition the position of the item in the new list * @return a payload object that represents the change between the two items. * 返回 一个 代表着新老item的改变内容的 payload对象, */ @nullable @override public object getchangepayload(int olditemposition, int newitemposition) { //实现这个方法 就能成为文艺青年中的文艺青年 // 定向刷新中的部分更新 // 效率最高 //只是没有了itemchange的白光一闪动画,(反正我也觉得不太重要) testbean oldbean = molddatas.get(olditemposition); testbean newbean = mnewdatas.get(newitemposition); //这里就不用比较核心字段了,一定相等 bundle payload = new bundle(); if (!oldbean.getdesc().equals(newbean.getdesc())) { payload.putstring("key_desc", newbean.getdesc()); } if (oldbean.getpic() != newbean.getpic()) { payload.putint("key_pic", newbean.getpic()); } if (payload.size() == 0)//如果没有变化 就传空 return null; return payload;// }
简单的说,这个方法返回一个object类型的payload,它包含了某个item的变化了的那些内容。
我们这里使用bundle保存这些变化。
在adapter里如下重写三参的onbindviewholder:
@override public void onbindviewholder(diffvh holder, int position, list<object> payloads) { if (payloads.isempty()) { onbindviewholder(holder, position); } else { //文艺青年中的文青 bundle payload = (bundle) payloads.get(0); testbean bean = mdatas.get(position); for (string key : payload.keyset()) { switch (key) { case "key_desc": //这里可以用payload里的数据,不过data也是新的 也可以用 holder.tv2.settext(bean.getdesc()); break; case "key_pic": holder.iv.setimageresource(payload.getint(key)); break; default: break; } } } }
这里传递过来的payloads是一个list,由注释可知,一定不为null,所以我们判断是否是empty,
如果是empty,就调用两参的函数,进行一次full bind。
如果不是empty,就进行partial bind,
通过下标0取出我们在getchangepayload方法里返回的payload,然后遍历payload的key,根据key检索,如果payload里携带有相应的改变,就取出来 然后更新在itemview上。
(这里,通过mdatas获得的也是最新数据源的数据,所以用payload的数据或者新数据的数据 进行更新都可以)
至此,我们已经掌握了刷新recyclerview,文艺青年中最文艺的那种写法。
四 在子线程中使用diffutil
在diffutil的源码头部注释中介绍了diffutil的相关信息,
diffutil内部采用的eugene w. myers's difference 算法,但该算法不能检测移动的item,所以google在其基础上改进支持检测移动项目,但是检测移动项目,会更耗性能。
在有1000项数据,200处改动时,这个算法的耗时:
打开了移动检测时:平均值:27.07ms,中位数:26.92ms。
关闭了移动检测时:平均值:13.54ms,中位数:13.36ms。
有兴趣可以自行去源码头部阅读注释,对我们比较有用的是其中一段提到,
如果我们的list过大,这个计算出diffresult的时间还是蛮久的,所以我们应该将获取diffresult的过程放到子线程中,并在主线程中更新recyclerview。
这里我采用handler配合diffutil使用:
代码如下:
private static final int h_code_update = 1; private list<testbean> mnewdatas;//增加一个变量暂存newlist private handler mhandler = new handler() { @override public void handlemessage(message msg) { switch (msg.what) { case h_code_update: //取出result diffutil.diffresult diffresult = (diffutil.diffresult) msg.obj; diffresult.dispatchupdatesto(madapter); //别忘了将新数据给adapter mdatas = mnewdatas; madapter.setdatas(mdatas); break; } } }; new thread(new runnable() { @override public void run() { //放在子线程中计算diffresult diffutil.diffresult diffresult = diffutil.calculatediff(new diffcallback(mdatas, mnewdatas), true); message message = mhandler.obtainmessage(h_code_update); message.obj = diffresult;//obj存放diffresult message.sendtotarget(); } }).start();
就是简单的handler使用,不再赘述。
五总结和其他
1 其实本文代码量很少,可下载demo查看,一共就四个类。
但是不知不觉又被我写的这么长,主要涉及到了一些源码的注释的翻译,方便大家更好的理解。
2 diffutil很适合下拉刷新这种场景,
更新的效率提高了,而且带动画,而且~还不用你动脑子算了。
不过若是就做个删除 点赞这种,完全不用diffutils。自己记好postion,判断一下postion在不在屏幕里,调用那几个定向刷新的方法即可。
3 其实diffutil不是只能和recyclerview.adapter配合使用,
我们可以自己实现 listupdatecallback接口,利用diffutil帮我们找到新旧数据集的最小差异集 来做更多的事情。
4 注意 写demo的时候,用于比较的新老数据集,不仅arraylist不同,里面每个data也要不同。 否则changed 无法触发。
实际项目中遇不到,因为新数据往往是网络来的。
5 今天是中秋节的最后一天,我们公司居然就开始上班了!!!气愤之余,我怒码一篇diffutil,我都不需要用diffutil,也能轻易比较出我们公司和其他公司的差异。qaq,而且今天状态不佳,居然写了8个小时才完工。本以为这篇文章是可以入选微作文集的,没想到也是蛮长的。没有耐心的其实可以下载demo看看,代码量没多少,使用起来还是很轻松的。
github传送门:
https://github.com/mcxtzhang/diffutils
以上就是对android7.0 工具类diffutil 的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!