轉載請標明出處: http://www.lxweimin.com/p/b888d6d17b5a
本文出自:【張旭童的簡書】 (http://www.lxweimin.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點個star。多謝
https://github.com/mcxtzhang/SupportDemos
背景:
打算寫一個系列了,講解Android Support包內那些常用or冷門有用的工具類的合集。
最近leader在優化IM會話列表,同事以前的做法是無腦notifyDatasetChanged()刷新RecyclerView的。
在消息聊得很嗨很多的時候,界面頻繁刷新,會話列表會出現丟失焦點現象。且性能畢竟不高。
遂想采用定向刷新。
同事知道我以前研究過DiffUtil和定向刷新相關內容,于是便和我討論。
(不知道DiffUtil的點這里)http://www.lxweimin.com/p/9b6e12d8eea0
(不了解定向刷新的點這里)http://www.lxweimin.com/p/1ac13f74da63
由于IM會話列表是從數據庫里讀的,他還告訴我會有數據集重復的現象,且會話列表肯定是按時間排序的,所以這對我們的數據組織提出了兩點要求:有序、去重。
我的想法是:
- 采用DiffUtil自動計算新老數據集差異,然后自動完成定向刷新。
- 至于數據集的去重和有序,我打算用TreeSet去幫助我們做。
利用Set本身元素不重復的特性,加之Tree的有序性,來解決數據組織的兩個需求。
可是leader不知道從哪搜出來一個SortedList,告訴我這是Android SDK提供的。也可以完成排序and去重。
我心說這是哪路神仙,我以為是JDK給的呢,于是也查閱了一番資料,遂揭開了SortedList的神秘面紗,也有了今天的文章。
轉載請標明出處: http://www.lxweimin.com/p/b888d6d17b5a
本文出自:【張旭童的簡書】 (http://www.lxweimin.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點個star。多謝
https://github.com/mcxtzhang/SupportDemos
SortedList是什么?
源碼頭注釋如下:
A Sorted list implementation that can keep items in order and also notify for changes in the list。
翻譯:
一個有序列表(數據集)的實現,可以保持ItemData都是有序的,并(自動)通知列表(RecyclerView)(數據集)中的更改。
人話:
首先它是一種數據結構,是一個有序的List,list改變后(增刪改查),也可以一直保持數據集List有序,并且會自動調用adapter中定向更新notifyXXXX方法,更新RecyclerView。
對了,它還會自動去重。
關鍵點:
搭配RecyclerView使用,去重,有序,自動定向刷新
剛看到這里,我覺得這特么自動定向刷新這一點特性,怎么有點像DiffUtil,后來我查閱資料才發現,這家伙出來的比DiffUtil要早,是在Support Library 22 引入的。所以說應該是DiffUtil像它。
而且SortedList 和 DiffUtil 內部 都實現、持有了一些共同的接口,暴漏出供我們重寫比較規則的Callback的方法名都幾乎一毛一樣。
我個人感覺SortedList從設計上和DiffUtil比,是有一點點不足,這可能也是官方后來又在Support Library 24 中引入DiffUtil的一個理由吧。具體異同,稍后總結。先看怎么用吧。

用法:
我們來看看如果使用SortedList該怎么寫:
Adapter:
要寫RecyclerView,就少不了Adapter。
一個常規的Adapter內部一般持有一個List<T>
的數據集,
使用SortedList
的話,需要將存儲數據源的變量類型改變成SortedList,
- 唯一差異:將以前的ArrayList->替換為SortedList.
其他的話,倒沒有變化,因為SortedList雖然沒有繼承自List,但是暴漏出API還和List一樣的。
public class SortedAdapter extends RecyclerView.Adapter<SortedAdapter.VH> {
/**
* 數據源替換為SortedList,
* 以前可能會用ArrayList。
*/
private SortedList<TestSortBean> mDatas;
...
public SortedAdapter(Context mContext, SortedList<TestSortBean> mDatas) {
this.mContext = mContext;
this.mDatas = mDatas;
mInflater = LayoutInflater.from(mContext);
}
public void setDatas(SortedList<TestSortBean> mDatas) {
this.mDatas = mDatas;
}
@Override
public SortedAdapter.VH onCreateViewHolder(ViewGroup parent, int viewType) {
return new SortedAdapter.VH(mInflater.inflate(R.layout.item_diff, parent, false));
}
@Override
public void onBindViewHolder(final SortedAdapter.VH holder, final int position) {
TestSortBean bean = mDatas.get(position);
holder.tv1.setText(bean.getName());
holder.tv2.setText(bean.getId() + "");
holder.iv.setImageResource(bean.getIcon());
}
...
}
實體類
無任何修改,就是一個普通的實體類。與上文DiffUtil里的一樣。
Callback:
看過DiffUtil詳解的同學對這個Callback的編寫和理解就易如反掌了,編寫規則和套路和DiffUtil.Callback一樣。
而且還少寫一個方法public Object getChangePayload(int oldItemPosition, int newItemPosition)
,這里順帶復習一下上文內容,這個方法返回 一個 代表著新老item的改變內容的 payload對象
這里說遠一點,關于這個少寫的方法,正是定向刷新中部分綁定(Partial bind)的核心方法。
DiffUtil
是利用這個getChangePayload()
方法的返回值,作為第三個參數,回調ListUpdateCallback
接口里的void onChanged(int position, int count, Object payload);
方法,最終回調adapter.notifyItemRangeChanged(position, count, payload);
方法,再往下就走到Adapter的三參數onBindViewHolder(VH holder, int position, List<Object> payloads)
方法,也就是我們部分綁定所操作的地方了,不太明白的可以去看DiffUtil詳解.
除此之外,耶不用傳新舊數據集進來了,里面的每個方法都是直接傳入ItemData進行比較。
那么我們的SortedList的Callback如下編寫:
public class SortedListCallback extends SortedListAdapterCallback<TestSortBean> {
public SortedListCallback(RecyclerView.Adapter adapter) {
super(adapter);
}
/**
* 把它當成equals 方法就好
*/
@Override
public int compare(TestSortBean o1, TestSortBean o2) {
return o1.getId() - o2.getId();
}
/**
* 和DiffUtil方法一致,用來判斷 兩個對象是否是相同的Item。
*/
@Override
public boolean areItemsTheSame(TestSortBean item1, TestSortBean item2) {
return item1.getId() == item2.getId();
}
/**
* 和DiffUtil方法一致,返回false,代表Item內容改變。會回調mCallback.onChanged()方法;
*/
@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;
}
}
Activity:
Activity的編寫也沒啥大變化,區別如下:
- 以前構建Adapter時,一般會將data也一起傳入,現在可傳可不傳。
- SortedList初始化的時候,要將Adapter傳進來。所以先構建Adapter,再構建SortedList
public class SortedListActivity extends AppCompatActivity {
/**
* 數據源替換為SortedList,
* 以前可能會用ArrayList。
*/
private SortedList<TestSortBean> mDatas;
private RecyclerView mRv;
private SortedAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sorted_list);
mRv = (RecyclerView) findViewById(R.id.rv);
mRv.setLayoutManager(new LinearLayoutManager(this));
//★以前構建Adapter時,一般會將data也一起傳入,現在有變化
mAdapter = new SortedAdapter(this, null);
mRv.setAdapter(mAdapter);
initData();
mAdapter.setDatas(mDatas);
}
private void initData() {
//★SortedList初始化的時候,要將Adapter傳進來。所以先構建Adapter,再構建SortedList
mDatas = new SortedList<>(TestSortBean.class, new SortedListCallback(mAdapter));
mDatas.add(new TestSortBean(10, "Android", R.drawable.pic1));
//★注意這里有一個重復的字段 會自動去重的。
mDatas.add(new TestSortBean(10, "Android重復", R.drawable.pic1));
mDatas.add(new TestSortBean(2, "Java", R.drawable.pic2));
mDatas.add(new TestSortBean(30, "背鍋", R.drawable.pic3));
mDatas.add(new TestSortBean(4, "手撕產品", R.drawable.pic4));
mDatas.add(new TestSortBean(50, "手撕測試", R.drawable.pic5));
}
代碼寫到這里,界面就可以正常顯示了。效果如Gif圖。
可以看到雖然我們add進去的數據 是有重復的,順序也是亂序的。
但是列表界面依然按照id的升序顯示。
到這就完了嗎,還沒有說到自動定向刷新呢。
0步自動定向刷新
和DiffUtil兩步完成定向刷新比,SortedList這一點真的是很強。0步完成自動定向刷新。
新增一條:
在上述代碼的基礎上,如果此時查詢數據庫,發現有一條新的IM聊天信息,那么直接add()
進來即可:
add 內部會自動調用 mCallback.onInserted(index, 1)
->notifyItemRangeInserted(index,1)
也就是說我們add一次 它就notify一次,沒有batch操作,有點low
mDatas.add(new TestSortBean(26, "溫油對待產品", R.drawable.pic6));//模擬新增
mDatas.add(new TestSortBean(12, "小馬可以來點贊了", R.drawable.pic6));//模擬新增
mDatas.add(new TestSortBean(2, "Python", R.drawable.pic6));//add進去 重復的會自動修改
新增一坨:
如果是一坨消息,可以用addAll()
,查看源碼,它內部會自動做Batch操作,beginBatchedUpdates();
和endBatchedUpdates();
。所以如果想batch,就必須用addAll()操作,感覺這算一個限制。
//addAll 也分兩種
//第一種 以可變參數addAll
//mDatas.addAll(new TestSortBean(26, "帥", R.drawable.pic6),new TestSortBean(27, "帥", R.drawable.pic6));
//第二種 集合形式
List<TestSortBean> temp = new ArrayList<>();
temp.add(new TestSortBean(26, "帥", R.drawable.pic6));
temp.add(new TestSortBean(28, "帥", R.drawable.pic6));
mDatas.addAll(temp);
刷新
而如果是刷新的場景,可能就不太適用了,刷新時,服務器給我們的一般都是一個List,直接addAll 要先clear, 會閃屏:
List<TestSortBean> newDatas = new ArrayList<>();
for (int i = 0; i < mDatas.size(); i++) {
try {
newDatas.add(mDatas.get(i).clone());//clone一遍舊數據 ,模擬刷新操作
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
newDatas.add(new TestSortBean(29, "帥", R.drawable.pic6));//模擬新增數據
newDatas.get(0).setName("Android+");
newDatas.get(0).setIcon(R.drawable.pic7);//模擬修改數據
TestSortBean testBean = newDatas.get(1);//模擬數據位移
newDatas.remove(testBean);
newDatas.add(testBean);
mDatas.clear();
mDatas.addAll(newDatas);
異步操作
查看源碼,SortedList是在每次add()
、addAll()
、clear()
.....等對數據集進行增刪改查的函數里,都會進行一遍排序和去重。這排序和去重顯然是個耗時操作。那么我想說能不能用異步處理呢?丟在子線程中。
于是我如下寫:
//每次add都會計算一次 想放在子線程中
new Thread(new Runnable() {
@Override
public void run() {
mDatas.add(new TestSortBean(26, "帥", R.drawable.pic6));//模擬新增數據
mDatas.add(new TestSortBean(27, "帥", R.drawable.pic6));//模擬新增數據
}
}).start();
}
然而這是肯定不行的,上文提過,每次add 會自動 mAdapter.notifyItemRangeInserted(position, count);
在線程中操作UI,會android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
這一點就不如DiffUtil啦。
和DiffUtil的異同
它們兩真的很像,而且不管是DiffUtil
計算出Diff后,還是SortedList
修改過數據后,內部持有的回調接口都是同一個:android.support.v7.util.ListUpdateCallback
:
/**
* An interface that can receive Update operations that are applied to a list.
* <p>
* This class can be used together with DiffUtil to detect changes between two lists.
*/
public interface ListUpdateCallback {
void onInserted(int position, int count);
void onRemoved(int position, int count);
void onMoved(int fromPosition, int toPosition);
void onChanged(int position, int count, Object payload);
}
我就不解析源碼了,非常簡單,大致流程就是:
DiffUtil
計算出Diff
或者SortedList
察覺出數據集有改變后,在合適的時機,回調ListUpdateCallback
接口的這四個方法,DiffUtil
和SortedList
提供的默認Callback實現中,都會通知Adapter完成定向刷新。
這就是自動定向刷新的原理。
總結一下它們的異同吧:
- DiffUtil比較兩個數據源(一般是List)的差異(Diff),Callback中比對時 傳遞的參數是 position
- SortedList 能完成數據集的排序 和去重, Callback中比對時,傳遞的是ItemData (JavaBean)。
- DiffUtil 能完成自動定向刷新 + 部分綁定
- SortedList 只能完成自動定向刷新
- DiffUtil 更通用,SortedList還與數據結構耦合
- DiffUtils: 檢測不出重復的,會被認為是新增的。(因為比對的核心是postion。 所以無法去重) 但是IM這種消息順序移動會被檢測到。
- 它們都是一種自動定向刷新的手段
感受總結:
使用SortedList的話,Adapter的保存數據集的變量類型要改變。
對代碼有侵入性,沒有熱插拔的快感。
在項目中有各種BaseAdapter的前提下,可能要擴展一種BaseSortedListAdater更方便使用。
只不過它的目的不是在定向刷新,而是維護數據集的 有序 & 去重 。
順帶有一個定向刷新的功能。
而DiffUtil主打的就是 比較集合的差異,更是幫我們自動完成定向刷新。
所以SortedList 不適用于 服務器給所有數據過來的,下拉刷新情況。此時不如使用普通的List。
它的亮點和核心,還是在于 有序 & 去重 。
且它也不支持 部分綁定(Partial bind)。
但它在特定場景下,例如 數據集每次更新時是增量更新,且需要維持一個排序規則的時候,就像城市列表界面,還是給我們帶來了一定的便利之處的。
轉載請標明出處: http://www.lxweimin.com/p/b888d6d17b5a
本文出自:【張旭童的簡書】 (http://www.lxweimin.com/users/8e91ff99b072/latest_articles)
代碼傳送門:喜歡的話,隨手點個star。多謝
https://github.com/mcxtzhang/SupportDemos