本文主要结合通讯录刷新以及IM会话场景实例思考列表的更新数据性能优化,另外介绍列表控件如ListView、RecyclerView的局部刷新方法。
通讯录-利用DiffUtil优化下拉刷新
通常我们加载一个列表,调用mAdapter.notifyDataSetChanged();进行粗暴的刷新,这种方式刷新整个列表控件,并且无法响应RecyclerView的动画,用户体验不是很好。
而今天要介绍的DiffUtil将解决这些痛点,自动帮我们调用下面的方法,达到优雅的刷新效果。
mAdapter.notifyItemRangeInserted(position, count);
mAdapter.notifyItemRangeRemoved(position, count);
mAdapter.notifyItemMoved(fromPosition, toPosition);
mAdapter.notifyItemRangeChanged(position, count, payload);
本身RecyclerView的适配器提供一些增删移动的局部刷新方法的,但是很多时候我们不确定新的数据和旧的数据差异性,导致有些小伙伴还是粗暴的removeAll,然后addAll新的数据(实际上就是一次replace动作)。比方说刷新动作时,原始数据20条,新的数据来了30条,可能存在有相同的数据,但是仅仅某些字段发生改变。此时无法单纯的在末尾添加新数据,通常移除旧数据,然后添加所有新的数据。对于用户来说,我可见的只有一个屏幕,有些数据可能在当前屏幕没有出现或者发生改变,上面这种方式会让用户感觉整个界面闪动,并且重新加载了一遍,用户体验不是很好。
通讯录往往是更新字段为主,新数据在使用应用时间较长后会达到一个稳定值,不会频繁有新的数据进入。按照常规逻辑,一般进入界面拉取本地数据库的数据,更新界面,然后等待网络数据到达时,进行替换。在网络数据替换的过程中,很多时候屏幕内的数据可能没有发生变化,但是进行replace操作会导致闪动,尤其是头像被重新加载(如果本地没做缓存的话,又是流量的损耗)。所以优雅的只更新数据且屏幕内的列表item只刷新特定item,甚至于单个item的某个控件,这样对于用户来说,这是一次很不错的文艺青年体验。下面开始介绍DiffUtils的文艺使用,来替换传统的屌丝刷新。
DiffUtil结构说明:
1、DiffUtil:核心类,做新旧数据的对比,以及回调更新接口,其中calculateDiff方法用来进行新旧数据集的比较。
2、DiffUtil.Callback:DiffUtil里的一个接口,用来判断新旧数据是否相等,或者更新了什么内容。实际使用过程时,需要编写一个该接口的实现类。
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的内容是否有变化
/*此方法返回值不为空(不是null)时,
*判定是否整个item刷新,还是更新item里的某一个控件
*adapter可以通过onBindViewHolder(ViewHolder holder, int position, List
3、ListUpdateCallback:提供一个刷新的接口
4、BatchingListUpdateCallback:ListUpdateCallback的实现类,处理局部刷新业务逻辑
使用方式:
1、按照传统写法写好ViewHolder、Adapter以及RecyclerView绑定代码。
2、开始植入DiffUtil,先编写一个DiffUtil.CallBack的实现类。getChangePayload方法只有areItemsTheSame返回true、areContentsTheSame返回false时触发,用来回传给Adapter一些更新后的字段值
public class DiffCallBack extends DiffUtil.Callback {
private List mOldDatas, mNewDatas;//看名字
public DiffCallBack(List mOldDatas, List 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).getId()==mNewDatas.get(newItemPosition).getId();
}
/**
* 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
}
if (!beanOld.getName().equals(beanNew.getName())) {
return false;//如果有内容不同,就返回false
}
return true; //默认两个data内容是相同的
}
/**
* 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 (!oldBean.getName().equals( newBean.getName())) {
payload.putString("KEY_NAME", newBean.getName());
}
if (payload.size() == 0) {//如果没有变化 就传空
return null;
}
return payload;//
}
}
3、下面介绍核心调用入口,用来触发新旧数据的比较以及更新。第一个方法是默认进行检测item的移动,不过会影响算法性能。第二个方法是核心的算法所在。
/**
* Calculates the list of update operations that can covert one list into the other one.
*
* @param cb The callback that acts as a gateway to the backing list data
*
* @return A DiffResult that contains the information about the edit sequence to convert the
* old list into the new list.
*/
public static DiffResult calculateDiff(Callback cb) {
return calculateDiff(cb, true);
}
/**
* Calculates the list of update operations that can covert one list into the other one.
*
* If your old and new lists are sorted by the same constraint and items never move (swap
* positions), you can disable move detection which takes O(N^2)
time where
* N is the number of added, moved, removed items.
*
* @param cb The callback that acts as a gateway to the backing list data
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
*
* @return A DiffResult that contains the information about the edit sequence to convert the
* old list into the new list.
*/
public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
//.....do something
}
4、然后等待calculateDiff计算出差值DiffUtil.DiffResult(需要一定耗时,所以建议放在子线程),在传统的萌萌哒的notifyDataSetChanged方法处,更换为DiffUtil的dispatchUpdatesTo方法。下面实例是用RxJava在子线程计算,然后主线程更新适配器。
Observable.create(new Observable.OnSubscribe() {
@Override
public void call(Subscriber super DiffUtil.DiffResult> subscriber) {
//放在子线程中计算DiffResult
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
subscriber.onNext(diffResult);
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1() {
@Override
public void call(DiffUtil.DiffResult diffResult) {
//利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年
diffResult.dispatchUpdatesTo(mAdapter);
//别忘了将新数据给Adapter
mDatas = mNewDatas;
mAdapter.setDatas(mDatas);
}
});
上述内容已经解决了刷新时,新旧数据集的对比、定向、部分刷新。但是在IM会话界面,我们通常面临着新数据和旧数据的排序性(比如按时间排序),所以还需要进一步优化,以便能快速处理频繁每批次单条IM数据的接收。
IM会话列表-利用SortedList优化新数据插入
在IM会话界面,我们通常面临着同一时间段收到多条不同客户端发来的IM消息的场景。由于IM会话具备有序性特征,单纯使用DiffUtil无法让新消息接收时,插入的消息按发送时间排序。个别情况下,客户端弱网可能由于误发、重发导致多条相同消息,所以IM会话列表还需要具备去重性。所以在这种情况下,新消息的插入场景很适合使用SortedList,来做多条无序新消息插入到原先的有序消息列表。
1、使用准备,引入SortedList
private SortedList mDatas;
public SortedAdapter(Context mContext, SortedList mDatas) {
this.mContext = mContext;
this.mDatas = mDatas;
mInflater = LayoutInflater.from(mContext);
}
2、实现判断是否相同item的callback
ublic class SortedListCallback extends SortedListAdapterCallback {
/**
* Creates a {@link SortedList.Callback} that will forward data change events to the provided
* Adapter.
*
* @param adapter The Adapter instance which should receive events from the SortedList.
*/
public SortedListCallback(RecyclerView.Adapter adapter) {
super(adapter);
}
/**
* 把它当成equals 方法就好
*/
@Override
public int compare(TestSortBean o1, TestSortBean o2) {
return o1.getId() - o2.getId();
}
/**
* 和DiffUtil方法一致,不再赘述
*/
@Override
public boolean areItemsTheSame(TestSortBean item1, TestSortBean item2) {
return item1.getId() == item2.getId();
}
/**
* 和DiffUtil方法一致,不再赘述
*/
@Override
public boolean areContentsTheSame(TestSortBean oldItem, TestSortBean newItem) {
//默认相同 有一个不同就是不同
if (oldItem.getId() != newItem.getId()) {
return false;
}
if (!oldItem.getName().equals(newItem.getName())) {
return false;
}
if (oldItem.getIcon() != newItem.getIcon()) {
return false;
}
return true;
}
}
3、最后直接使用mData调用add方法插入新数据,或者更新旧数据即可。
TestSortBean newBean = new TestSortBean(integer, "我是手动加入的" + mEtId.getText(), getImgId
(integer % 10));
int index = mDatas.indexOf(newBean);
//从已有数据里寻找是否有该数据了,如果有,就执行更新
if (index<0){
mDatas.add(newBean);
}else {
mDatas.updateItemAt(index,newBean);
}
//也可以使用addAll
add方法注释里推荐如果原始列表存在新数据,使用updateItemAt替代add。
/**
* Adds the given item to the list. If this is a new item, SortedList calls
* {@link Callback#onInserted(int, int)}.
*
* If the item already exists in the list and its sorting criteria is not changed, it is
* replaced with the existing Item. SortedList uses
* {@link Callback#areItemsTheSame(Object, Object)} to check if two items are the same item
* and uses {@link Callback#areContentsTheSame(Object, Object)} to decide whether it should
* call {@link Callback#onChanged(int, int)} or not. In both cases, it always removes the
* reference to the old item and puts the new item into the backing array even if
* {@link Callback#areContentsTheSame(Object, Object)} returns false.
*
* If the sorting criteria of the item is changed, SortedList won't be able to find
* its duplicate in the list which will result in having a duplicate of the Item in the list.
* If you need to update sorting criteria of an item that already exists in the list,
* use {@link #updateItemAt(int, Object)}. You can find the index of the item using
* {@link #indexOf(Object)} before you update the object.
*
* @param item The item to be added into the list.
*
* @return The index of the newly added item.
* @see Callback#compare(Object, Object)
* @see Callback#areItemsTheSame(Object, Object)
* @see Callback#areContentsTheSame(Object, Object)}
*/
这里add方法是每次add或者update时就会更新数据,然后自动调用下面方法进行局部更新
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
在每次add时,都会在主线程进行排序等操作,一开始考虑到性能是否会有影响,但是从测试结果来看,不用担心。下面是10000条原始数据时,新增多条数据处理时的耗时。
数据量 | DiffUtils | SortedList |
---|---|---|
10000 | 5ms | 1ms |
add方法里会判断是否是原始数据已经存在,然后通知对应的方法执行局部刷新
private int add(T item, boolean notify) {
int index = findIndexOf(item, mData, 0, mSize, INSERTION);
if (index == INVALID_POSITION) {
index = 0;
} else if (index < mSize) {
T existing = mData[index];
if (mCallback.areItemsTheSame(existing, item)) {
if (mCallback.areContentsTheSame(existing, item)) {
//no change but still replace the item
mData[index] = item;
return index;
} else {
mData[index] = item;
mCallback.onChanged(index, 1);
return index;
}
}
}
addToData(index, item);
if (notify) {
mCallback.onInserted(index, 1);
}
return index;
}
总结:SortedList的单条add方法会先判断是否有存在数据,如果有就更新,不然就插入数据。add方法主要是用数组拷贝的方法进行进行插入操作。而addAll方法,会先拷贝新数据到数组,然后Arrays.sort(newItems, mCallback)进行排序,然后用duplicate方法进行去重。如果旧集合size为0,则全部插入;否则进行merge合并,然后逐个遍历插入或者更新(此处逻辑类似add单条数据)
DiffUtils和SortedList两者使用场景的对比总结
DiffUtils在做差异对比后,会使用新的datas作为数据源,此时新数据里不存在的旧数据会被移除,所以适用于下拉刷新整个界面的动作。比如一开始提到的通讯录列表,已进入界面时,需要从本地拉取数据以及在网络请求后再次刷新。而SortedList适合用于原始数据稳定且需要继续保留,然后新增多条或者反复新增单条数据的场景,且数据需要有序性。比如上面的IM会话列表,在保持局部刷新的同时还需要维持现有列表的有序性,防止新增加来的多条无序数据打乱列表。如果单纯一个满足不了需求,可以结合一起做,或者使用下面介绍的局部刷新技巧来达到效果。
局部刷新某个item方式
RecyclerView可以使用下面方法,或者使用上面DiffUtils里介绍的payload方式。关于RecyclerView的局部item刷新上面已经介绍,就不贴出来。
---1----
CouponVH couponVH = (CouponVH) mRv.findViewHolderForLayoutPosition(mSelectedPos);
if (couponVH != null) {//还在屏幕里
couponVH.ivSelect.setSelected(false);
}else {
//一些极端情况,holder被缓存在Recycler的cacheView里,
//此时拿不到ViewHolder,但是也不会回调onBindViewHolder方法。所以add一个异常处理
notifyItemChanged(mSelectedPos);
}
mDatas.get(mSelectedPos).setSelected(false);//不管在不在屏幕里 都需要改变数据
//设置新Item的勾选状态
mSelectedPos = position;
mDatas.get(mSelectedPos).setSelected(true);
holder.ivSelect.setSelected(true);
-----2---
if (mSelectedPos != position) {
//先取消上个item的勾选状态
mDatas.get(mSelectedPos).setSelected(false);
//传递一个payload
Bundle payloadOld = new Bundle();
payloadOld.putBoolean("KEY_BOOLEAN", false);
notifyItemChanged(mSelectedPos, payloadOld);
//设置新Item的勾选状态
mSelectedPos = position;
mDatas.get(mSelectedPos).setSelected(true);
Bundle payloadNew = new Bundle();
payloadNew.putBoolean("KEY_BOOLEAN", true);
notifyItemChanged(mSelectedPos, payloadNew);
}
@Override
public void onBindViewHolder(CouponVH holder, int position, List
ListView的部分刷新策略
//方法一:局部item整体刷新
/**
* 局部更新数据,调用一次getView()方法;Google推荐的做法
*
* @param listView 要更新的listview
* @param position 要更新的位置
*/
public void notifyDataSetChanged(ListView listView, int position) {
if (listView == null) {
return;
}
/**第一个可见的位置**/
int firstVisiblePosition = listView.getFirstVisiblePosition();
/**最后一个可见的位置**/
int lastVisiblePosition = listView.getLastVisiblePosition();
/**在看见范围内才更新,不可见的滑动后自动会调用getView方法更新**/
if (position >= firstVisiblePosition && position <= lastVisiblePosition) {
/**获取指定位置view对象**/
View view = listView.getChildAt(position - firstVisiblePosition);
getView(position, view, listView);
}
}
//方法二:定向刷新
//如果 当前选中的View 在当前屏幕可见,且不是自己,要定向刷新一下之前的View的状态
if (position != mSelectedPos) {
int firstPos = mLv.getFirstVisiblePosition() - mLv.getHeaderViewsCount();//这里考虑了HeaderView的情况
int lastPos = mLv.getLastVisiblePosition() - mLv.getHeaderViewsCount();
if (mSelectedPos >= firstPos && mSelectedPos <= lastPos) {
View lastSelectedView = mLv.getChildAt(mSelectedPos - firstPos);//取出选中的View
CouponVH lastVh = (CouponVH) lastSelectedView.getTag();
lastVh.ivSelect.setSelected(false);
}
//不管在屏幕是否可见,都需要改变之前的data
mDatas.get(mSelectedPos).setSelected(false);
//改变现在的点击的这个View的选中状态
couponVH.ivSelect.setSelected(true);
mDatas.get(position).setSelected(true);
mSelectedPos = position;
}
DEMO地址待更新
借鉴相关文章:
【Android】 RecyclerView、ListView实现单选列表的优雅之路.
http://blog.csdn.net/zxt0601/article/details/52703280
【Android】详解7.0带来的新工具类:DiffUtil
http://blog.csdn.net/zxt0601/article/details/52562770
【Android】你可能不知道的Support(一) 0步自动定向刷新:SortedList http://blog.csdn.net/zxt0601/article/details/53495709