关于通信录以及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…

相关文章
相关标签/搜索