RecyclerView DiffUtil使用 和源码

一个问题:

每次从服务器取到数据后,都是调用adapter.notifyDataSetChanged();进行刷新。那局部刷新(adapter.notifyItemChanged();)的这些东西不是白瞎了吗?对性能也不好,还没有动画。

怎么办:

用DiffUtil吧!号称可以进行局部刷新神器,让你的item 该刷新的地方就刷新,数据没有改变的地方不刷新(DiffUtil 内部调用了的局部刷新,还支持item动画哟!)。

怎么用:

  • 重写一个类:DiffUtil.Callback ,自己写,(注意打Log, 如果觉得自己菜的话)
    public class MyDiffCallback extends DiffUtil.Callback {
  //Thing 是adapter 的数据类,要换成自己的adapter 数据类
    private List<Thing> current;
    private List<Thing> next;

    public MyDiffCallback(List<Thing> current, List<Thing> next) {
        this.current = current;
        this.next = next;
        Log.d("数据c", current.toString());
        Log.d("数据n", next.toString());
    }

    /**
     * 旧数据的size
     */
    @Override
    public int getOldListSize() {
        return current.size();
    }

    /**
     * 新数据的size
     */
    @Override
    public int getNewListSize() {
        return next.size();
    }

    /**
     * 这个方法自由定制 ,
     * 在对比数据的时候会被调用
     * 返回 true 被判断为同一个item
     */
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        Thing currentItem = current.get(oldItemPosition);
        Thing nextItem = next.get(newItemPosition);

        return currentItem.getId() == nextItem.getId();
    }

    /**
     *在上面的方法返回true 时,
     * 这个方法才会被diff 调用
     * 返回true 就证明内容相同
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        Thing currentItem = current.get(oldItemPosition);
        Thing nextItem = next.get(newItemPosition);
        return currentItem.equals(nextItem);
    }
    }
  • 并创建它,
    MyDiffCallback callback = new MyDiffCallback(adapter.things, things);
    对比数据
    DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
  • 然后刷新,完事儿
    result.dispatchUpdatesTo(adapter);

源码分析,使用请回:

使用起来,尤其面临大量数据刷新时,你会感到从所未有的高效和简洁但是呢?
why are you so niu bi ?
read the fucking source code
想了解原理只有阅读源码。阅读源码时,有一点讲究,不是拿着一个类直接阅读,而是揪其一点不断深入。

比如他是怎么对比的:

一般我看不懂英文,我会先点开这个方法,看见这个方法内部调用了一个方法:

image.png

点入new AdapterListUpdateCallback(adapter)

意外收获

发现有一个adapter 传入这个对象,看源码它把adapter 包了一层,实现了接口,调用了adapter局部刷新的方法。
难道dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));里面通过这个包裹实现了局部刷新的?

 public final class AdapterListUpdateCallback implements ListUpdateCallback {
  @NonNull
  private final RecyclerView.Adapter mAdapter;

/**
 * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
 *
 * @param adapter The Adapter to send updates to.
 */
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
    mAdapter = adapter;
}

/** {@inheritDoc} */
@Override
public void onInserted(int position, int count) {
    mAdapter.notifyItemRangeInserted(position, count);
}

/** {@inheritDoc} */
@Override
public void onRemoved(int position, int count) {
    mAdapter.notifyItemRangeRemoved(position, count);
}

/** {@inheritDoc} */
@Override
public void onMoved(int fromPosition, int toPosition) {
    mAdapter.notifyItemMoved(fromPosition, toPosition);
}

/** {@inheritDoc} */
@Override
public void onChanged(int position, int count, Object payload) {
    mAdapter.notifyItemRangeChanged(position, count, payload);
}

}

进入方法 dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));

public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
        final BatchingListUpdateCallback batchingCallback;
        if (updateCallback instanceof BatchingListUpdateCallback) {
            //赋值
            batchingCallback = (BatchingListUpdateCallback) updateCallback;
        } else {
            // 转换赋值
            batchingCallback = new BatchingListUpdateCallback(updateCallback);
       
            //noinspection UnusedAssignment
            updateCallback = batchingCallback;
        }
...
 if (endX < posOld) {
                  //传入这个方法,发现其实在里面有调用局部刷新等方法
                dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
            }
...
            if (endY < posNew) {
              //传入这个方法,发现其实在里面有调用局部刷新等方法
                dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
                        endY);
            }
...
            for (int i = snakeSize - 1; i >= 0; i--) {
                if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
                      //这里也是
                    batchingCallback.onChanged(snake.x + i, 1,
                            mCallback.getChangePayload(snake.x + i, snake.y + i));
                }
            }

         后面省略.....
      batchingCallback.dispatchLastEvent();
    }

我们发现BatchingListUpdateCallback类中这个dispatchLastEvent()方法中调用局部刷新,然后在 上面这个方法dispatchUpdatesTo(ListUpdateCallback updateCallback)最后一行调用了dispatchLastEvent()方法,代码中dispatchAdditions()dispatchRemovals()方法中均有调用batchingCallback 的刷新方法,意外收获局部刷新的秘密!

回到正题,他到底是怎样高效对比数据的呢?

查看完整代码

    public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
      //跳过
        final BatchingListUpdateCallback batchingCallback;
        if (updateCallback instanceof BatchingListUpdateCallback) {
            batchingCallback = (BatchingListUpdateCallback) updateCallback;
        } else {
            batchingCallback = new BatchingListUpdateCallback(updateCallback);
            // replace updateCallback with a batching callback and override references to
            // updateCallback so that we don't call it directly by mistake
            //noinspection UnusedAssignment
            updateCallback = batchingCallback;
        }
        //跳过
        final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
        int posOld = mOldListSize;
        int posNew = mNewListSize;
        for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
            //这是啥?好像是通过他的 x y 进行刷新的,具体不晓得了
            final Snake snake = mSnakes.get(snakeIndex);
            final int snakeSize = snake.size;
            final int endX = snake.x + snakeSize;
            final int endY = snake.y + snakeSize;
            if (endX < posOld) {
                //进入方法,一通操作,除了刷新,没有数据对比
                dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
            }

            if (endY < posNew) {
             //进入方法,一通操作,除了刷新,没有数据对比
                dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
                        endY);
            }
            for (int i = snakeSize - 1; i >= 0; i--) {
                if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
                    // 没有数据对比
                    batchingCallback.onChanged(snake.x + i, 1,
                   //进入方法,一通操作,除了刷新,没有数据对比
                            mCallback.getChangePayload(snake.x + i, snake.y + i));
                }
            }
            posOld = snake.x;
            posNew = snake.y;
        }
        batchingCallback.dispatchLastEvent();
    }

看一遍你会得到上面一样结果,这时候会注意到自己找的地方错了,好像是数据对比在这之前,而且会注意到final Snake snake = mSnakes.get(snakeIndex);这个对象,后面的代码是依照Snake 的x.y 等属性进行刷新,可以推断出mSnackes集合必然保存着对比数据后产生的结果,但是看它的注释一脸懵逼。

// The Myers' snakes. At this point, we only care about their diagonal sections.
// 迈尔斯的蛇。在这一点上,我们只关心它们的对角线部分。(对于这个集合的注释一脸懵逼)
    private final List<Snake> mSnakes;

通过这个集合,发现构造函数中对它进行的赋值:

    DiffResult(Callback callback, List<Snake> snakes, int[] oldItemStatuses,
            int[] newItemStatuses, boolean detectMoves) {
        //集合被赋值
        mSnakes = snakes;
      ···
        // 里面有add(snacke) 操作,但是看方法注释是针对于0 的情况,直接忽略。
       addRootSnake();
         // 这方法里面 调用了重写的areContentsTheSame(oldItemPos, newItemPos)(对比item内容是否有改变)
         //,并把状态存入数组,用的时候直接取出。
         findMatchingItems();
    }

追踪构造函数(DiffResult())被调用之处是calculateDiff(Callback cb, boolean detectMoves)
发现calculateDiff(Callback cb, boolean detectMoves)方法
DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); 调用(我们创建的时候,手动调的,请看前面使用部分文章)

数据如何对比依然是谜

推测知道snakes 集合是数据对比后的结果,它用来判断item是否直接刷新
追踪snakes集合产生过程必然就知道了对比过程
接着看caulateDiff(callback cb ,boolean deteMoves)方法

public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
    // 这里调用了我们使用时重写的方法,现在知道,重写的方法的用处了
    final int oldSize = cb.getOldListSize();
    final int newSize = cb.getNewListSize();
    //结果集
    final List<Snake> snakes = new ArrayList<>();
      ···
      // 一通操作

     ···  
      // 得到snake   ,重要
        final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
                range.newListStart, range.newListEnd, forward, backward, max);
        if (snake != null) {
            if (snake.size > 0) {
                //添加到集合
                snakes.add(snake);
            }
           //转成 x ,y 信息得到 
            snake.x += range.oldListStart;
            snake.y += range.newListStart;
          
            // 一通转化 和保存,返回数据结果集
            ····
             return new DiffResult(cb, snakes, forward, backward, detectMoves);

}

发现diffPartial(cb, range.oldListStart, range.oldListEnd,range.newListStart, range.newListEnd, forward, backward, max);方法,计算得snake 对象然后添加到集合中, 但是打开这个方法里面你会发现里面全是计算:

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) {
           ···
            while (x < oldSize && y < newSize
                         //这里调用了重写的方法耶,判断是否是同一个item
                    && cb.areItemsTheSame(startOld + x, startNew + y)) {
                x++;
                y++;
            }}}
          
        for (int k = -d; k <= d; k += 2) {
           for (int k = -d; k <= d; k += 2) {
         ···
            while (x > 0 && y > 0
                   //这里调用了重写的方法耶,判断是否是同一个item
                    && cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
                x--;
                y--;
            }
       
        }
    }
     ···
}

不必担心,这里涉及一个算法就是Myers diff 算法(我打算单独分析),snakes集合目前的话可以理解为保存的对比结果就行了。

到这里diffUtil 的源码才算大概了解了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,165评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,476评论 3 405
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,446评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,157评论 1 301
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,164评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,615评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,969评论 3 430
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,959评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,495评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,529评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,641评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,233评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,976评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,407评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,552评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,218评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,715评论 2 366

推荐阅读更多精彩内容