DiffUtils讓你的RecyclerView如斯順滑

不做一知半解的小路發表於2019-10-22

DiffUtils讓你的RecyclerView如斯順滑

前言

RecyclerView的出現讓我們可以實現更多更復雜的滑動佈局,包括不同的佈局型別,不同的資料型別。但是,越是複雜的佈局,出現卡頓的現象就會越發的明顯。

這其中不乏有以下幾點:

  1. 無效的測量佈局繪製
  2. 模版的重複初始化

通過滑動的日誌分析,我們可以發現同一模版在上滑下滑的同時,會重新走onBindView方法,即使這一模版內容沒有任何變化的情況下。如果在這個方法中所要執行的邏輯很多,這將會導致卡頓的出現。

原理

那麼為何會重新走onBindView方法呢,你可能會說去看原始碼就知道了呀。沒錯,當你不知道它是如何實現的時候,去看原始碼往往是最直接有效的。但是今天這個並不是這篇文章的重點,關於RecyclerView的複用和回收網上有很多原始碼的解析,這裡就不一一貼原始碼解釋了,只是做一些簡單的介紹。

DiffUtils讓你的RecyclerView如斯順滑

  1. RecyclerView的回收以及複用的都是ViewHolder而不是View。
  2. RecyclerView只是一個ViewGroup,其真正實現滑動的是在LayoutManager中。
  3. 回收:當一個itemView不可見時,會將其放到記憶體中,以便實現複用。
  4. 複用:四重奏,mChangedScrapmCacheViews、開發者自定義以及 RecycledViewPool中,都沒有才會onCreatViewHolder
  5. RecyclerViewPool中的儲存方式是 viewType-Array,也就是對對於每種型別最多存5個。

大部分的快取是從recyclerViewPool中拿的,recyclerViewPool一定會走onBindViewHolder方法。這也就是回答了我們上面的提問,所以我們的思路就來了,可以通過判斷資料的變化來控制onBindView中相應邏輯的執行,來提升效能。

DiffUtil主要是和RecyclerView或者ListView配合使用,由DiffUtil找出每個item的變化,由RecyclerView。Adapter更新UI。

如果對DiffUtil不熟悉的,可以先去看這篇文章RecyclerView資料更新神器 - DiffUtil

這次優化的思路就是在onBindviewHolder中判斷新舊item的變化,來做到精準更新。

實現

判斷新舊資料的不同,如果資料比較複雜,那麼該怎麼去判斷呢?我們可以用幾個主要的欄位來概括一下這個資料。

public interface IElement {

    /**
     * 資料內容
     * @return 返回該資料體區別於其他資料體的內容
     */
    String diffContent();
}
複製程式碼

所有的資料bean要實現這個介面,然後在diffContent中定義自己的主要欄位。不實現這個介面,用DiffUtils是沒有意義的。

我們從設定資料的步驟來一步步講解,方便理解。

當資料從網路請求回來之後,走refreshDataSource方法。

    /**
     * 重新整理列表
     *
     * @param pagedList 新的列表資料
     */
    public final void refreshDataSource(List<DATA> pagedList) {
        mDiffer.submitList(pagedList);
    }
複製程式碼

submitList中對新舊資料進行對比,並將對比結果提供給Adapter。

    /**
     *  比較資料差異,分發差異結果,呼叫區域性重新整理API,每次請求介面增加一次版本號
     * @param newList  新的資料來源
     */
    public void submitList(final List<T> newList) {
        if (newList == mList) {
            // 嘗試將渲染完成時機通知出去
            return;
        }

        final int runGeneration = ++mMaxScheduledGeneration;
        // 如果新集合是空 就把老集合所有都remove
        if (newList == null) {
            int countRemoved = mList.size();
            mList = null;
            mUpdateCallback.onRemoved(0, countRemoved);
            return;
        }
        // 如果老集合是空 就把新集合所有都insert
        if (mList == null) {
            mList = newList;
            updateDataSource(Collections.unmodifiableList(newList));
            mConfig.getBackgroundThreadExecutor()
                    .execute(
                            new Runnable() {
                                @SuppressLint("RestrictedApi")
                                @Override
                                public void run() {
                                    for (int i = 0; i < newList.size(); i++) {
                                        final T t = newList.get(i);
                                        if(t!=null){
                                            dataElementCache.putRecord(new ElementRecord(IDHelper.getUniqueId(t),t));
                                        }
                                    }
                                    dataElementCache.copySelf();
                                }
                            });
            mUpdateCallback.onInserted(0, newList.size());

            return;
        }

        final List<T> oldList = mList;
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @SuppressLint("RestrictedApi")
            @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) {
                        return mConfig.getDiffCallback().areItemsTheSame(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }

                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().areContentsTheSame(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }
                    // payload可以理解為關鍵的資料,就是新老item的資料中 到底哪裡變化了,區域性重新整理某個item -- 預設返回null
                    @Override
                    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().getChangePayload(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }
                });
                mConfig.getMainThreadExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            //重新整理佈局
                            diffResult.dispatchUpdatesTo(mUpdateCallback);
                        }
                    }
                });
            }
        });
    }
複製程式碼

其中updateDataSource是用來更新資料來源,確保拿到的是最新的。

dataElementCache中存放的是:

private volatile ConcurrentMap<IElement,ElementRecord> elementRecords = new ConcurrentHashMap<>();
複製程式碼

ElementRecord記錄了當前的資料以及唯一標示UniqueId,將主要欄位以md5方式呈現,減少耗時。

這裡提供了非同步進行資料比較的邏輯,mUpdateCallback實現ListUpdateCallback介面,實現adpter的重新整理功能。

    @Override
    public void onChanged(int position, int count, Object payload) {
        recordChanged(position,count);
        super.onChanged(position, count, payload);
    }

    @Override
    public void onInserted(int position, int count) {
        recordChanged(position, count);
        super.onInserted(position, count);
    }

    private void recordChanged(int position, int count) {
        int  tempPosition = position;
        for (int i = 0; i <count; i++) {
            // SparseArray
            changedPositions.put(tempPosition,tempPosition);
            tempPosition++;
        }
    }
複製程式碼

更新UI必須要在主執行緒中,但是DiffUtil是一個耗時的操作,所以這次用的是它的一個封裝類AsyncListDifferConfig

首先,在初始化中新建Differ物件。

    /**
     * 資料項比較工具
     */
    private final IDataDiff mDataDiff;

    /** 資料比較工具 */
    private final AsyncListDifferDelegate<DATA> mDiffer;

    private final IDataCache<DATA> dataElementCache;

    public BaseSwiftAdapter(Context mContext) {
        this.mContext = mContext;
        dataElementCache = new ElementCache<>();
        final DiffCallBack diffCallBack = new DiffCallBack(dataElementCache);

        @SuppressLint("RestrictedApi") AsyncDifferConfig config =
                new AsyncDifferConfig.Builder<>(diffCallBack)
                        .setBackgroundThreadExecutor(AppExecutors.backGroudExecutors)
                        .setMainThreadExecutor(AppExecutors.mainExecutors)
                        .build();
        ChangeListCallback changedPositionCallback = new ChangeListCallback(this);
        mDataDiff = new DataDiffImpl<>(changedPositionCallback, dataElementCache);
        mDiffer =
                new AsyncListDifferDelegate(changedPositionCallback, config, dataElementCache);
    }
複製程式碼

AsyncListDifferConfig需要三個引數:DiffUtil的內部類ItemCallback、diffUtil的item比較執行緒、主執行緒。

ItemCallback是它的抽象內部類,也就是mConfig.getDiffCallback(),看下它要實現的幾個方法:

   @Override
    public boolean areItemsTheSame(IElement oldItem, IElement newItem) {
        return areContentsTheSame(oldItem, newItem);
    }

    /**
     * 總體思想是先比較物件地址,在比較內容,提高比較效率
     *
     * @param oldItem
     * @param newItem
     * @return
     */
    @Override
    public boolean areContentsTheSame(IElement oldItem, IElement newItem) {
        if (newItem == null) {
            return true;
        }
        if (oldItem == newItem) {
            return true;
        }
        recordNewElement(newItem);
        final String newContent = newItem.diffContent();
        if(newContent == null || "".equals(newContent)){
            return false;
        }

        return newContent.equals(oldItem.diffContent());
    }
複製程式碼

areItemTheSame和areContentsTheSame,都是用來判斷新舊資料是否相同,所以這裡用了同一個邏輯,diffContent中存放該資料具有影響的幾個欄位相拼接的字串。

dataElementCache用來儲存所有資料的集合型別是IElement-ElementRecord的Array。IElement是資料本身,ElementRecord是資料的記錄集,包含資料以及資料的唯一標示。

mDiffer會在後續中講到。

我們來看下關鍵的onBindViewHolder中所做的事情:

    @Override
    public final void onBindViewHolder(VH holder, int position) {
        if (null != holder && holder.itemView != null) {
            tryBindData(holder, position, this.getItem(position));
        }
    }
    
    private void tryBindData(VH holder, int position, DATA newData) {
      final ElementRecord oldDataRecord = holder.content();
      boolean needBind ;
      if(needBind = (hasPositionDataRefreshChanged(oldDataRecord == null ? null : (DATA) oldDataRecord.getElement(), newData, position) || oldDataRecord == null) ){
            Log.d(getClass().getName(),"adapter onBindData 重新整理或者新建"+ holder.getItemViewType());
        }else if(needBind =  hasDataContentChanged(oldDataRecord,newData)){
            Log.d(getClass().getName(),"adapter onBindData 滑動內容改變"+ holder.getItemViewType());
        }
        if(needBind){
            refreshAndBind(holder, position, newData);
        }else {
            Log.d(getClass().getName(),"adapter onBindData 複用不重新整理"+ holder.getItemViewType());
        }
    }
複製程式碼

先去判斷是否是重新整理變化,其次去判斷是否是滑動變化,如果有變化就重新整理佈局,否則什麼也不做。

    private boolean hasPositionDataRefreshChanged(DATA oldItem, DATA newItem, int position){
        return  mDataDiff.areItemsChanged(oldItem, newItem, position);
    }
    private boolean hasDataContentChanged(ElementRecord oldItem, DATA newItem){
        return  mDataDiff.areContentsChanged(oldItem, newItem);
    }
複製程式碼

可以看出mDataDiff主要用來判斷新舊資料是否相同。我們來實現mDataDiff中的比較:

    @Override
    public boolean areItemsChanged(T oldItem, T newItem, int position) {
        boolean changed = changedPositionCallback.hasPositionChanged(position);
        if(changed){
            changedPositionCallback.removeChangedPosition(position);
        }
        return changed;
    }

    @Override
    public boolean areContentsChanged(ElementRecord oldElementRecord, T newItem) {
        return oldElementRecord !=null  && oldElementRecord.getElement() != newItem && newItem!=null && !sameContent(oldElementRecord,newItem);
    }


    private boolean sameContent(ElementRecord oldElementRecord, T newItem){
        final ElementRecord newElementRecord = dataCache.getRecord(newItem);
        if(newElementRecord == null){
            return false;
        }
        if(IDHelper.forceRefresh(newElementRecord) || IDHelper.forceRefresh(oldElementRecord)){
            return false;
        }
        return newElementRecord.getUniqueId().equals(oldElementRecord.getUniqueId());
    }
複製程式碼

其中比較思路為:先判斷該viewHolder是否在changedPositions中,changedPositions由ChangeListCallback來提供並實現。其次判斷兩個物件以及唯一標示。

這裡用到了兩個比較類:一個是ItemCallback的比較類以及mDataDiff比較類,這裡容易看混。

最關鍵的程式碼在這句:

diffResult.dispatchUpdatesTo(mUpdateCallback);
複製程式碼

diffResult會將最小變化量提供給adpter,讓其實現區域性重新整理。

總結

到了這裡,我要講的就差不多要結束了,希望對你們有所幫助。謝謝你們看到了這裡。

參考資料

RecyclerView資料更新神器 - DiffUtil

相關文章