RecyclerView DiffUtil使用 和源碼

一個(gè)問(wèn)題:

每次從服務(wù)器取到數(shù)據(jù)后筑累,都是調(diào)用adapter.notifyDataSetChanged();進(jìn)行刷新。那局部刷新(adapter.notifyItemChanged();)的這些東西不是白瞎了嗎籽前?對(duì)性能也不好江滨,還沒(méi)有動(dòng)畫(huà)津肛。

怎么辦:

用DiffUtil吧盒让!號(hào)稱(chēng)可以進(jìn)行局部刷新神器,讓你的item 該刷新的地方就刷新缭乘,數(shù)據(jù)沒(méi)有改變的地方不刷新(DiffUtil 內(nèi)部調(diào)用了的局部刷新沐序,還支持item動(dòng)畫(huà)喲!)堕绩。

怎么用:

  • 重寫(xiě)一個(gè)類(lèi):DiffUtil.Callback 策幼,自己寫(xiě),(注意打Log奴紧, 如果覺(jué)得自己菜的話)
    public class MyDiffCallback extends DiffUtil.Callback {
  //Thing 是adapter 的數(shù)據(jù)類(lèi)特姐,要換成自己的adapter 數(shù)據(jù)類(lèi)
    private List<Thing> current;
    private List<Thing> next;

    public MyDiffCallback(List<Thing> current, List<Thing> next) {
        this.current = current;
        this.next = next;
        Log.d("數(shù)據(jù)c", current.toString());
        Log.d("數(shù)據(jù)n", next.toString());
    }

    /**
     * 舊數(shù)據(jù)的size
     */
    @Override
    public int getOldListSize() {
        return current.size();
    }

    /**
     * 新數(shù)據(jù)的size
     */
    @Override
    public int getNewListSize() {
        return next.size();
    }

    /**
     * 這個(gè)方法自由定制 ,
     * 在對(duì)比數(shù)據(jù)的時(shí)候會(huì)被調(diào)用
     * 返回 true 被判斷為同一個(gè)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 時(shí)黍氮,
     * 這個(gè)方法才會(huì)被diff 調(diào)用
     * 返回true 就證明內(nèi)容相同
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        Thing currentItem = current.get(oldItemPosition);
        Thing nextItem = next.get(newItemPosition);
        return currentItem.equals(nextItem);
    }
    }
  • 并創(chuàng)建它唐含,
    MyDiffCallback callback = new MyDiffCallback(adapter.things, things);
    對(duì)比數(shù)據(jù)
    DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
  • 然后刷新,完事兒
    result.dispatchUpdatesTo(adapter);

源碼分析,使用請(qǐng)回:

使用起來(lái)沫浆,尤其面臨大量數(shù)據(jù)刷新時(shí)捷枯,你會(huì)感到從所未有的高效和簡(jiǎn)潔但是呢?
why are you so niu bi ?
read the fucking source code
想了解原理只有閱讀源碼专执。閱讀源碼時(shí)淮捆,有一點(diǎn)講究,不是拿著一個(gè)類(lèi)直接閱讀他炊,而是揪其一點(diǎn)不斷深入。

比如他是怎么對(duì)比的:

一般我看不懂英文已艰,我會(huì)先點(diǎn)開(kāi)這個(gè)方法痊末,看見(jiàn)這個(gè)方法內(nèi)部調(diào)用了一個(gè)方法:

image.png

點(diǎn)入new AdapterListUpdateCallback(adapter)

意外收獲

發(fā)現(xiàn)有一個(gè)adapter 傳入這個(gè)對(duì)象,看源碼它把a(bǔ)dapter 包了一層哩掺,實(shí)現(xiàn)了接口凿叠,調(diào)用了adapter局部刷新的方法。
難道dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));里面通過(guò)這個(gè)包裹實(shí)現(xiàn)了局部刷新的?

 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);
}

}

進(jìn)入方法 dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));

public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
        final BatchingListUpdateCallback batchingCallback;
        if (updateCallback instanceof BatchingListUpdateCallback) {
            //賦值
            batchingCallback = (BatchingListUpdateCallback) updateCallback;
        } else {
            // 轉(zhuǎn)換賦值
            batchingCallback = new BatchingListUpdateCallback(updateCallback);
       
            //noinspection UnusedAssignment
            updateCallback = batchingCallback;
        }
...
 if (endX < posOld) {
                  //傳入這個(gè)方法盒件,發(fā)現(xiàn)其實(shí)在里面有調(diào)用局部刷新等方法
                dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
            }
...
            if (endY < posNew) {
              //傳入這個(gè)方法蹬碧,發(fā)現(xiàn)其實(shí)在里面有調(diào)用局部刷新等方法
                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();
    }

我們發(fā)現(xiàn)BatchingListUpdateCallback類(lèi)中這個(gè)dispatchLastEvent()方法中調(diào)用局部刷新,然后在 上面這個(gè)方法dispatchUpdatesTo(ListUpdateCallback updateCallback)最后一行調(diào)用了dispatchLastEvent()方法,代碼中dispatchAdditions()dispatchRemovals()方法中均有調(diào)用batchingCallback 的刷新方法炒刁,意外收獲局部刷新的秘密恩沽!

回到正題,他到底是怎樣高效對(duì)比數(shù)據(jù)的呢翔始?

查看完整代碼

    public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
      //跳過(guò)
        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;
        }
        //跳過(guò)
        final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
        int posOld = mOldListSize;
        int posNew = mNewListSize;
        for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
            //這是啥罗心?好像是通過(guò)他的 x y 進(jìn)行刷新的,具體不曉得了
            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) {
                //進(jìn)入方法城瞎,一通操作渤闷,除了刷新,沒(méi)有數(shù)據(jù)對(duì)比
                dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
            }

            if (endY < posNew) {
             //進(jìn)入方法脖镀,一通操作飒箭,除了刷新,沒(méi)有數(shù)據(jù)對(duì)比
                dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
                        endY);
            }
            for (int i = snakeSize - 1; i >= 0; i--) {
                if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
                    // 沒(méi)有數(shù)據(jù)對(duì)比
                    batchingCallback.onChanged(snake.x + i, 1,
                   //進(jìn)入方法蜒灰,一通操作弦蹂,除了刷新,沒(méi)有數(shù)據(jù)對(duì)比
                            mCallback.getChangePayload(snake.x + i, snake.y + i));
                }
            }
            posOld = snake.x;
            posNew = snake.y;
        }
        batchingCallback.dispatchLastEvent();
    }

看一遍你會(huì)得到上面一樣結(jié)果卷员,這時(shí)候會(huì)注意到自己找的地方錯(cuò)了盈匾,好像是數(shù)據(jù)對(duì)比在這之前,而且會(huì)注意到final Snake snake = mSnakes.get(snakeIndex);這個(gè)對(duì)象毕骡,后面的代碼是依照Snake 的x.y 等屬性進(jìn)行刷新削饵,可以推斷出mSnackes集合必然保存著對(duì)比數(shù)據(jù)后產(chǎn)生的結(jié)果,但是看它的注釋一臉懵逼未巫。

// The Myers' snakes. At this point, we only care about their diagonal sections.
// 邁爾斯的蛇窿撬。在這一點(diǎn)上,我們只關(guān)心它們的對(duì)角線部分叙凡。(對(duì)于這個(gè)集合的注釋一臉懵逼)
    private final List<Snake> mSnakes;

通過(guò)這個(gè)集合劈伴,發(fā)現(xiàn)構(gòu)造函數(shù)中對(duì)它進(jìn)行的賦值:

    DiffResult(Callback callback, List<Snake> snakes, int[] oldItemStatuses,
            int[] newItemStatuses, boolean detectMoves) {
        //集合被賦值
        mSnakes = snakes;
      ···
        // 里面有add(snacke) 操作,但是看方法注釋是針對(duì)于0 的情況握爷,直接忽略跛璧。
       addRootSnake();
         // 這方法里面 調(diào)用了重寫(xiě)的areContentsTheSame(oldItemPos, newItemPos)(對(duì)比item內(nèi)容是否有改變)
         //,并把狀態(tài)存入數(shù)組新啼,用的時(shí)候直接取出追城。
         findMatchingItems();
    }

追蹤構(gòu)造函數(shù)(DiffResult())被調(diào)用之處是calculateDiff(Callback cb, boolean detectMoves)
發(fā)現(xiàn)calculateDiff(Callback cb, boolean detectMoves)方法
DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); 調(diào)用(我們創(chuàng)建的時(shí)候,手動(dòng)調(diào)的燥撞,請(qǐng)看前面使用部分文章)

數(shù)據(jù)如何對(duì)比依然是謎

推測(cè)知道snakes 集合是數(shù)據(jù)對(duì)比后的結(jié)果座柱,它用來(lái)判斷item是否直接刷新
追蹤snakes集合產(chǎn)生過(guò)程必然就知道了對(duì)比過(guò)程
接著看caulateDiff(callback cb ,boolean deteMoves)方法

public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
    // 這里調(diào)用了我們使用時(shí)重寫(xiě)的方法迷帜,現(xiàn)在知道,重寫(xiě)的方法的用處了
    final int oldSize = cb.getOldListSize();
    final int newSize = cb.getNewListSize();
    //結(jié)果集
    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);
            }
           //轉(zhuǎn)成 x ,y 信息得到 
            snake.x += range.oldListStart;
            snake.y += range.newListStart;
          
            // 一通轉(zhuǎn)化 和保存戏锹,返回?cái)?shù)據(jù)結(jié)果集
            ····
             return new DiffResult(cb, snakes, forward, backward, detectMoves);

}

發(fā)現(xiàn)diffPartial(cb, range.oldListStart, range.oldListEnd,range.newListStart, range.newListEnd, forward, backward, max);方法,計(jì)算得snake 對(duì)象然后添加到集合中火诸, 但是打開(kāi)這個(gè)方法里面你會(huì)發(fā)現(xiàn)里面全是計(jì)算:

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
                         //這里調(diào)用了重寫(xiě)的方法耶锦针,判斷是否是同一個(gè)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
                   //這里調(diào)用了重寫(xiě)的方法耶,判斷是否是同一個(gè)item
                    && cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
                x--;
                y--;
            }
       
        }
    }
     ···
}

不必?fù)?dān)心惭蹂,這里涉及一個(gè)算法就是Myers diff 算法(我打算單獨(dú)分析)伞插,snakes集合目前的話可以理解為保存的對(duì)比結(jié)果就行了。

到這里diffUtil 的源碼才算大概了解了盾碗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末媚污,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子廷雅,更是在濱河造成了極大的恐慌耗美,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件航缀,死亡現(xiàn)場(chǎng)離奇詭異商架,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)芥玉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)蛇摸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人灿巧,你說(shuō)我怎么就攤上這事赶袄。” “怎么了抠藕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵饿肺,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我盾似,道長(zhǎng)敬辣,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任零院,我火速辦了婚禮溉跃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘告抄。我一直安慰自己撰茎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布玄妈。 她就那樣靜靜地躺著乾吻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拟蜻。 梳的紋絲不亂的頭發(fā)上绎签,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音酝锅,去河邊找鬼诡必。 笑死,一個(gè)胖子當(dāng)著我的面吹牛搔扁,可吹牛的內(nèi)容都是我干的爸舒。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼稿蹲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扭勉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起苛聘,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涂炎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后设哗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體唱捣,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年网梢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了震缭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡战虏,死狀恐怖拣宰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情活烙,我是刑警寧澤徐裸,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站啸盏,受9級(jí)特大地震影響重贺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜回懦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一气笙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怯晕,春花似錦潜圃、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)堵第。三九已至,卻和暖如春隧出,著一層夾襖步出監(jiān)牢的瞬間踏志,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工胀瞪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留针余,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓凄诞,卻偏偏與公主長(zhǎng)得像圆雁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子帆谍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容