局部刷新神器的使用—— AsyncListDiffer

前言

RecyclerView刷新时,一行代码,简单方便adapter.notifyDataSetChanged()
无脑的刷新了一遍整个RecyclerView,新老数据一样时,本来不用刷新,但却刷新了。而且也不会触发RecyclerView的动画(删除、新增、位移、change动画)。效率较低。在这里插入图片描述
Adapter 对此有四个函数

  adapter.notifyItemRangeChanged();
  adapter.notifyItemRangeInserted();
  adapter.notifyItemRangeRemoved();
  adapter.notifyItemMoved();

局部刷新只会刷新指定position的item,解决了上述简单粗暴的刷新方式。
我想要单纯的刷新Item的控件?Adapter还有这样的函数吗?

DiffUtil

一 概述
使用DiffUtil后它用来比较两个数据集,寻找出旧数据集和新数据集的最小变化量。
二 使用步骤:
1.继承DiffUtil.Callback函数

public abstract static class Callback {
        public abstract int getOldListSize();
        public abstract int getNewListSize();
         /**
         *新老数据集Item是否是同一个 (一般比较的id)
         **/
        public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
        /**
        * 比较同一个item 的内容是否相同(上面方法返回true 时,会调用此方法)
        **/
        public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
         /**
        * Object就是表示Item改变了哪些内容(上面方法返回false 时,会调用此方法)
        **/
        @Nullable
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return null;
        }
    }
  1. 计算新旧数据集的差异 (Myers 差分算法 )
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack());

比较计算差异,用的Myers差分算法(后面介绍)

比较差异的源码,我也看不懂,别着急。(后面了解算法的思想)

 private static Snake diffPartial(Callback cb, int startOld, int endOld,
            int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
            ...
        for (int d = 0; d <= dLimit; d++) {
            for (int k = -d; k <= d; k += 2) {
                // find forward path
                // we can reach k from k - 1 or k + 1. Check which one is further in the graph
                int x;
                final boolean removal;
                if (k == -d || (k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1])) {
                    x = forward[kOffset + k + 1];
                    removal = false;
                } else {
                    x = forward[kOffset + k - 1] + 1;
                    removal = true;
                }
                // set y based on x
                int y = x - k;
                // move diagonal as long as items match
                while (x < oldSize && y < newSize
                        && cb.areItemsTheSame(startOld + x, startNew + y)) {
                    x++;
                    y++;
                }
                forward[kOffset + k] = x;
             
            for (int k = -d; k <= d; k += 2) {
                // find reverse path at k + delta, in reverse
                final int backwardK = k + delta;
                int x;
                final boolean removal;
                if (backwardK == d + delta || (backwardK != -d + delta
                        && backward[kOffset + backwardK - 1] < backward[kOffset + backwardK + 1])) {
                    x = backward[kOffset + backwardK - 1];
                    removal = false;
                } else {
                    x = backward[kOffset + backwardK + 1] - 1;
                    removal = true;
                }
                // set y based on x
                int y = x - backwardK;
                // move diagonal as long as items match
                while (x > 0 && y > 0
                        && cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
                    x--;
                    y--;
                }
                backward[kOffset + backwardK] = x;
                if (!checkInFwd && k + delta >= -d && k + delta <= d) {
                    if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
                        Snake outSnake = new Snake();
                        outSnake.x = backward[kOffset + backwardK];
                        outSnake.y = outSnake.x - backwardK;
                        outSnake.size =
                                forward[kOffset + backwardK] - backward[kOffset + backwardK];
                        outSnake.removal = removal;
                        outSnake.reverse = true;
                        return outSnake;
                    }
                }
            }
        }
     ...
    }

3.将Adapter传给DiffUtil.DiffResult

diffResult.dispatchUpdatesTo(Adapter);

根据差异情况,自动调用adapter这四个方法

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

Demo:

继承DiffUtil.Callback,重写Callback的方法(根据需要)

 @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        UserBean userBeanOld = oldList.get(oldItemPosition);
        UserBean userBeanNew = newList.get(newItemPosition);
        if (userBeanOld.id == userBeanNew.id){
            return true;
        }
        return  false;
    }
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        UserBean userBeanOld = oldList.get(oldItemPosition);
        UserBean userBeanNew = newList.get(newItemPosition);
        if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
            return  false;
        }
        if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
            return  false;
        }
        return true;
    }
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        UserBean userBeanOld = oldList.get(oldItemPosition);
        UserBean userBeanNew = newList.get(newItemPosition);
        Bundle bundle = new Bundle();
        if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
            bundle.putString("name",userBeanNew.name);
        }
        if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
            bundle.putString("tel",userBeanNew.tel);
        }
        return bundle;
    }
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) {
        if(payloads == null ||payloads.isEmpty() || payloads.get(0)== null){
            super.onBindViewHolder(holder, position, payloads);
        }
       else {
            Bundle bundle =(Bundle)payloads.get(0);
          String name =   bundle.getString("name");
          if(!TextUtils.isEmpty(name)){
              ((viewholder) holder).NameTextView.setText(name);
          }
            String tel = bundle.getString("tel");
            if(!TextUtils.isEmpty(tel)){
                ((viewholder) holder).TelTextView.setText(tel);
            }
        }
    }
 DiffUtilsCallbackDemo diffUtilsCallbackDemo=  new DiffUtilsCallbackDemo();
            diffUtilsCallbackDemo.setOldList(oldList);
            diffUtilsCallbackDemo.setNewList(newList);
            DiffUtil.DiffResult diffResult= DiffUtil.calculateDiff(diffUtilsCallbackDemo);
            diffResult.dispatchUpdatesTo(myAdapter);

在这里插入图片描述
计算的时间:

  <li>100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
 *     <li>100 items and 100 modifications: 3.82 ms, median: 3.75 ms
 *     <li>100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
 *     <li>1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
 *     <li>1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
 *     <li>1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
 *     <li>1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms

一般我们不在主线程计算数据,如果在主线程中计算的时间过长,可能出现ANR。因此就有了AsyncListDiffer。

AsyncListDiffer

使用步骤:
1.自实现DiffUtil.ItemCallback,给出item差异性计算条件
2.使用submitList()更新数据,并刷新ui

demo:

public class AsyncCallback extends DiffUtil.ItemCallback<UserBean> {

    @Override
    public boolean areItemsTheSame(@NonNull UserBean oldItem, @NonNull UserBean newItem) {
        return oldItem.id == newItem.id;
    }
    @Override
    public boolean areContentsTheSame(@NonNull UserBean userBeanOld, @NonNull UserBean userBeanNew) {
        if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
            return  false;
        }
        if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
            return  false;
        }
        return true;
    }
    @Override
    public Object getChangePayload(@NonNull UserBean userBeanOld, @NonNull UserBean userBeanNew) {
        Bundle bundle = new Bundle();
        if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
            bundle.putString("name",userBeanNew.name);
        }
        if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
            bundle.putString("tel",userBeanNew.tel);
        }
        return bundle;
    }
}

AsyncListDiffer内部自动维护数据集List
submitList()源码:

   @SuppressWarnings("WeakerAccess")
    public void submitList(@Nullable final List<T> newList,
            @Nullable final Runnable commitCallback) {
        // incrementing generation means any currently-running diffs are discarded when they finish
        final int runGeneration = ++mMaxScheduledGeneration;
       // 首先判断 newList 与 AsyncListDiffer 中缓存的数据集 mList 是否为同一个地址,如果是的话,直接返回不做任何处理。也就是说,调用 submitList() 方法所传递数据集时,需要new一个新的List。
        if (newList == mList) {
            if (commitCallback != null) {
                commitCallback.run();
            }
            return;
        }

        final List<T> previousList = mReadOnlyList;

        // 如果新数据集为空,此种情况不需要计算diff
        // 判断新数据集newList是否为null。若新数据集newList 为null,将移除所有 Item 的操作并分发给 ListUpdateCallback,让ListUpdateCallback执行onRemoved,直接清空数据即可(mList 置为 null),同时将只读List mReadOnlyList 清空
        if (newList == null) {
            //noinspection ConstantConditions
            int countRemoved = mList.size();
            mList = null;
            mReadOnlyList = Collections.emptyList();
            // notify last, after list is updated
            mUpdateCallback.onRemoved(0, countRemoved);
            onCurrentListChanged(previousList, commitCallback);
            return;
        }

        // 如果旧数据集为空,此种情况也不需要计算diff
        // 直接将新数据添加到旧数据集即可
        // 通知item insert 新数据的全部
        if (mList == null) {
            mList = newList;
            mReadOnlyList = Collections.unmodifiableList(newList);
            mUpdateCallback.onInserted(0, newList.size());
            onCurrentListChanged(previousList, commitCallback);
            return;
        }
        final List<T> oldList = mList;
        // 在子线程中计算DiffResult
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                    @Override
                    public int getOldListSize() {
                        return oldList.size();
                    }

                    @Override
                    public int getNewListSize() {
                        return newList.size();
                    }

                    @Override
                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                        T oldItem = oldList.get(oldItemPosition);
                        T newItem = newList.get(newItemPosition);
                        if (oldItem != null && newItem != null) {
                            return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
                        }
                        // If both items are null we consider them the same.
                        return oldItem == null && newItem == null;
                    }

                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                        T oldItem = oldList.get(oldItemPosition);
                        T newItem = newList.get(newItemPosition);
                        if (oldItem != null && newItem != null) {
                            return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
                        }
                        if (oldItem == null && newItem == null) {
                            return true;
                        }
                        // There is an implementation bug if we reach this point. Per the docs, this
                        // method should only be invoked when areItemsTheSame returns true. That
                        // only occurs when both items are non-null or both are null and both of
                        // those cases are handled above.
                        throw new AssertionError();
                    }

                    @Nullable
                    @Override
                    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                        T oldItem = oldList.get(oldItemPosition);
                        T newItem = newList.get(newItemPosition);
                        if (oldItem != null && newItem != null) {
                            return mConfig.getDiffCallback().getChangePayload(oldItem, newItem);
                        }
                        // There is an implementation bug if we reach this point. Per the docs, this
                        // method should only be invoked when areItemsTheSame returns true AND
                        // areContentsTheSame returns false. That only occurs when both items are
                        // non-null which is the only case handled above.
                        throw new AssertionError();
                    }
                });
				// 主线程中更新UI
                mMainThreadExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            latchList(newList, result, commitCallback);
                        }
                    }
                });
            }
        });
    }

Myers 差分算法(Myers Difference Algorithm)

Myers差分算法是由Eugene W.Myers在1986年发表的一篇论文中提出
DiffUtil 对比列表item 数据,git diff文件对比都用到了这个算法。
什么是diff?
diff 就是目标文本和源文本之间的差异。也就是将原文本变成目标文本的差异。

比较字符串A 和字符串B diff。

String A = "ABCABBA"
String B = "CBABAC"

一眼看不出来什么不同。那字符串A = 123 和字符串 B = 0123 呢 ? 字符串B比A多了字符0;
比较的依据 :计算最大重合部分

算法中的概念:
snake :一步所走的路径 为 snake。
k : 定义x 轴向右增大,y轴 向下增大,定义 k = x-y;
d : 定义为步数。(斜线不算步数)
在这里插入图片描述
对图的解释:
1.X坐标是字符串A,Y坐标是字符串 B;
2.(0,0)走到右下角;
3.下走一步是删除一个 A 里面的元素,右走一步是添加一个 B 里面的元素,斜线箭头是不操作。(尽可能走斜线);

这幅图可以非常简单的抽象成带权重的有向图:
假设横向纵向权值为 1,对角权值是 0,那么只要总和权重最低,这就是我们的最优解。这幅图中的坐标自变量是d和k,表示了在不同d取值以及不同的k取值最终可以到达的坐标。
目的:
然后找到最短的d(走尽可能多的斜线),到达我们要的终点。

我们从坐标 (0, 0) 开始,此时,d=0,k=0,然后逐步增加 d,计算每个 k 值下对应的最优坐标。
因为每一步要么向右(x + 1),要么向下(y + 1),对角线不影响路径长度,所以,当 d=1 时,k 只可能有两个取值,要么是 1,要么是 -1。
当 d=1,k=1 时,最优坐标是 (1, 0)。
当 d=1,k=-1 时,最优坐标是 (0, 1)。
因为 d=1 时,k 要么是 1,要么是 -1,当 d=2 时,表示在 d=1 的基础上再走一步,k 只有三个可能的取值,分别是 -2,0,2。
当 d=2,k=-2 时,最优坐标是 (2, 4)。
当 d=2,k=0 时,最优坐标是 (2, 2)。
当 d=2,k=2 时,最优坐标是 (3, 1)。在这里插入图片描述

以此类推,直到我们找到一个 d 和 k 值,达到最终的目标坐标 (7, 6)。

下图横轴代表 d,纵轴代表 k,中间是最优坐标,从这张图可以清晰的看出,当 d=5,k=1 时,我们到达了目标坐标 (7, 6),因此,”最短的直观的“路径就是 (0, 0) -> (1, 0) -> (3, 1) -> (5, 4) -> (7, 5) -> (7, 6)

论文中的图
对于步数的d的取值范围肯定是大于0小于字符串A于字符串B之和(即A全部删除,B全部添加) 0<= d<=M+N

k = x-y ; 我们可以通过x 求y ,v[]数组里面以k为index,存储最优坐标的x值,取的时候只要知道k值,因为v[k] =x;通过有y =v[k]-k 就可以算出y;
k 结合图形,我们从(0,0)开始,每次只增长一步,向下一步=>k-1,向右一步=>k+1;
第一步:k的极限,k-1<= k <=k+1
第二步:k的极限,k-2<= k <=k+2

第d步: k的极限,k-d<= k <= k+d

论文中的伪代码如下:

V[ 1 ] = 0;
//外层循环 d 步数 最多为全部删除,全部添加(N 为A序列长度, M 为B序列长度)
for ( int d = 0 ; d <= N + M ; d++ ){
// k 为斜线集合
  for ( int k = -d ; k <= d ; k += 2 ){
    // (遵循先删除后添加的原则)
    bool down = ( k == -d || ( k != d && V[ k - 1 ] < V[ k + 1 ] ) );

	//得到上一个的K值
    int kPrev = down ? k + 1 : k - 1;
    // 上一步的point(x,y)
    int xStart = V[ kPrev ];
    int yStart = xStart - kPrev;

    //中点
    int xMid = down ? xStart : xStart + 1;
    int yMid = xMid - k;

    //终点
    int xEnd = xMid;
    int yEnd = yMid;
    //斜线数量
    int snake = 0;
    //遇到斜线,(数据相等),然后,x,y 各加一
    while ( xEnd < N && yEnd < M && A[ xEnd ] == B[ yEnd ] ) { 
    xEnd++; 
    yEnd++; 
    snake++; }
    // 保存到数组
    V[ k ] = xEnd;
    
    //到了最终点(N,M),找到了最短步数的方案 跳出循环
    if ( xEnd >= N && yEnd >= M ) 
  }
}

参考链接:
[https://www.codeproject.com/Articles/42279/Investigating-Myers-diff-algorithm-Part-1-of-2#heading0014]
参考链接

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值