本文主要結合通信錄刷新以及IM會話場景實例思考列表的更新數據性能優化,另外介紹列表控件如ListView、RecyclerView的局部刷新方法。android
#通信錄-利用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結構說明: 一、DiffUtil:核心類,作新舊數據的對比,以及回調更新接口,其中calculateDiff方法用來進行新舊數據集的比較。 二、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<Object> payloads)三參數方法來更新,
*經過返回對應的字段值,讓界面刷新特定控件
*/
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
複製代碼
三、ListUpdateCallback:提供一個刷新的接口 四、BatchingListUpdateCallback:ListUpdateCallback的實現類,處理局部刷新業務邏輯bash
使用方式: 一、按照傳統寫法寫好ViewHolder、Adapter以及RecyclerView綁定代碼。 二、開始植入DiffUtil,先編寫一個DiffUtil.CallBack的實現類。getChangePayload方法只有areItemsTheSame返回true、areContentsTheSame返回false時觸發,用來回傳給Adapter一些更新後的字段值網絡
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).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. * <p> * 當{@link #areItemsTheSame(int, int)} 返回true,且{@link #areContentsTheSame(int, int)} 返回false時,DiffUtils會回調此方法, * 去獲得這個Item(有哪些)改變的payload。 * <p> * 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. * <p> * 例如,若是你用RecyclerView配合DiffUtils,你能夠返回 這個Item改變的那些字段, * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} 能夠用那些信息去執行正確的動畫 * <p> * 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;// } } 複製代碼
三、下面介紹核心調用入口,用來觸發新舊數據的比較以及更新。第一個方法是默認進行檢測item的移動,不過會影響算法性能。第二個方法是核心的算法所在。ide
/**
* 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.
* <p>
* 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 <code>O(N^2)</code> 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
}
複製代碼
四、而後等待calculateDiff計算出差值DiffUtil.DiffResult(須要必定耗時,因此建議放在子線程),在傳統的萌萌噠的notifyDataSetChanged方法處,更換爲DiffUtil的dispatchUpdatesTo方法。下面實例是用RxJava在子線程計算,而後主線程更新適配器。工具
Observable.create(new Observable.OnSubscribe<DiffUtil.DiffResult>() {
@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<DiffUtil.DiffResult>() {
@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,來作多條無序新消息插入到原先的有序消息列表。
一、使用準備,引入SortedList替代原有List ,做爲數據源
private SortedList<TestSortBean> mDatas;
public SortedAdapter(Context mContext, SortedList<TestSortBean> mDatas) {
this.mContext = mContext;
this.mDatas = mDatas;
mInflater = LayoutInflater.from(mContext);
}
複製代碼
二、實現判斷是否相同item的callback
ublic class SortedListCallback extends SortedListAdapterCallback<TestSortBean> {
/**
* 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;
}
}
複製代碼
三、最後直接使用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)}.
* <p>
* 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.
* <p>
* 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<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle payload = (Bundle) payloads.get(0);
if (payload.containsKey("KEY_BOOLEAN")) {
boolean aBoolean = payload.getBoolean("KEY_BOOLEAN");
holder.ivSelect.setSelected(aBoolean);
}
}
}
複製代碼
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實現單選列表的優雅之路. blog.csdn.net/zxt0601/art… 【Android】詳解7.0帶來的新工具類:DiffUtil blog.csdn.net/zxt0601/art… 【Android】你可能不知道的Support(一) 0步自動定向刷新:SortedList blog.csdn.net/zxt0601/art…