關於通信錄以及IM會話列表的優化思考

本文主要結合通信錄刷新以及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…

相關文章
相關標籤/搜索