RecyclerView - DiffUtil

DiffUtil

DiffUtilAndroid 工程師提供的用於規範使用 notify*() 方法刷新數據的工具類。算法

在使用 RecyclerView 時常常會用到以下方法更新數據:markdown

  • notifyItemRangeInserted()
  • notifyItemRangeRemoved()
  • notifyItemMoved()
  • notifyItemRangeChanged()

當某條數據發生變化(如移除、修改等)時調用以上方法可用於更新數據以及 UI 顯示。 想象以下場景,若列表中大量數據產生變化,怎麼辦呢?通常操做是調用它:異步

  • notifyDataSetChanged()

將列表從頭至尾無腦更新一遍,這時候就出現問題了:ide

  • 圖片閃爍
  • 性能低,觸發佈局從新繪製
  • item 動畫不會觸發

聯想實際開發中,列表刷新操做是否是就調用了 notifyDataSetChanged()函數

基於上述問題咱們有了更高效的解決方案,那就是 - DiffUtilDiffUtil 使用 Eugene W. Myers 的差分算法計算兩列數據之間的最小更新數,將舊的列表轉換爲新的列表,並針對不一樣的數據變化,執行不一樣的調用,而不是無腦的 notifyDataSetChanged()工具

關於 Eugene W. Myers 差分算法分析能夠參考這篇文章: Myers 差分算法 (Myers Difference Algorithm) —— DiffUtils 之核心算法(一)oop

DiffUtil 用法很簡單,一共三步:佈局

  • 計算新、舊數據間的最小更新數
  • 更新列表數據
  • 更新 RecyclerView

具體實現以下:post

//第一步:調用 DiffUtil.calculateDiff 計算最小數據更新數
val diffResult = DiffUtil.calculateDiff(object : Callback() {
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldStudentList[oldItemPosition].id == newStudentList[newItemPosition].id
    }

    override fun getOldListSize(): Int {
        return oldStudentList.size
    }

    override fun getNewListSize(): Int {
        return newStudentList.size
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldStudentList[oldItemPosition].name == newStudentList[newItemPosition].name
    }
})

//第二步:更新舊數據集合
oldStudentList.clear()
oldStudentList.addAll(newStudentList)

//第三部:更新 RecyclerView
diffResult.dispatchUpdatesTo(diffAdapter)
複製代碼

第二步、第三部都很簡單,主要來看下 DiffUtil.Callback 中四個方法的含義。性能

public abstract static class Callback {
    /**
     * 舊數據 size
     */
    public abstract int getOldListSize();

    /**
     * 新數據 size
     */
    public abstract int getNewListSize();

    /**
     * DiffUtil 調用判斷兩個 itemview 對應的數據對象是否同樣. 因爲 DiffUtil 是對兩個不一樣數據集合的對比, 因此比較對象引用確定是不行的, 通常會使用 id 等具備惟一性的字段進行比較.
    
     * @param oldItemPosition 舊數據集合中的下標
     * @param newItemPosition 新數據集合中的下標
     * @return True 返回 true 及判斷兩個對象相等, 反之則是不一樣的兩個對象.
     */
    public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

    /**
     * Diffutil 調用判斷兩個相同對象之間的數據是否不一樣. 此方法僅會在 areItemsTheSame() 返回 true 的狀況下被調用.
     *
     * @param oldItemPosition 舊數據集合中的下標
     * @param newItemPosition 新數據集合中用以替換舊數據集合數據項的下標
     * @return True 返回 true 表明 兩個對象的數據相同, 反之則有差異.
     */
    public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

    /**
     * 當 areItemsTheSame() 返回true areContentsTheSame() 返回 false 時, 此方法將被調用, 來完成局部刷新功能.
     */
    @Nullable
    public Object getChangePayload(int oldItemPosition, int newItemPosition);
}
複製代碼

各個方法的含義都加在註釋中了,理解不難,用法更簡單。

具體 DiffUtil 是怎麼更新 RecyclerView 的呢,看下 dispatchUpdatesTo() 方法中都作了什麼

public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
    dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}
複製代碼

adapter 做爲參數建立了一個類 AdapterListUpdateCallback 的對象,AdapterListUpdateCallback 類中實現的就是具體的更新操做了。

public final class AdapterListUpdateCallback implements ListUpdateCallback {

    @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, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}
複製代碼

注意:經過了解 DiffUtil 的工做模式,咱們瞭解到 DiffUtil 的經過對兩列數據進行對比,產生對比結果來更新列表的。也就是說即便只更新列表中某個對象中的某個元素也要提供新的列表給 DiffUtil。因此能夠想到在開發中最符合 DiffUtil 的使用場景應該就是列表數據的刷新了。若是隻是某個對象的變化本身調用 notifyItem*() 就能夠了,不必使用 DiffUil

AsyncListDiffer

因爲 DiffUtil 計算兩個數據集合是在主線程中計算的,那麼數據量大耗時的操做勢必會影響到主線程代碼的執行。Android 中最經常使用的異步執行耗時操做,更新主線程 UI 的方法就是用到 Handler 了。不過不用擔憂,貼心的 Android 工程師已經爲咱們考慮到了這中狀況並提供你了 ListAdapterAsyncListDiffer 兩個工具類。

AsyncListDiffer 爲例看下它的用法: 首先註冊 AsyncListDiffer 的實例對象,來看下它的構造函數:

public class AsyncListDiffer<T> {
    public AsyncListDiffer(@RecyclerView.Adapter adapter, DiffUtil.ItemCallback<T> diffCallback) {
    this(new AdapterListUpdateCallback(adapter),
            new AsyncDifferConfig.Builder<>(diffCallback).build());
    }
}
複製代碼

能夠看到 AsyncListDiffer 類聲明中有一個泛型,用於指定集合的數據類型。構造函數中接收兩個參數一個是 Adapter 對象。Adapter 對象的做用和 DiffUtil 中的做用同樣,做爲參數建立 AdapterListUpdateCallback 對象,在其內是具體執行列表更新的方法。

另外一個是 DiffUtil.ItemCallback 對象,其做爲參數構建了類 AsyncDifferConfig 的對象。

AsyncDifferConfig.class
public final class AsyncDifferConfig<T> {

    private final Executor mMainThreadExecutor;
    private final Executor mBackgroundThreadExecutor;
    private final DiffUtil.ItemCallback<T> mDiffCallback;

    public static final class Builder<T> {
        
        public AsyncDifferConfig<T> build() {
            if (mBackgroundThreadExecutor == null) {
                synchronized (sExecutorLock) {
                    if (sDiffExecutor == null) {
                        sDiffExecutor = Executors.newFixedThreadPool(2);
                    }
                }
                mBackgroundThreadExecutor = sDiffExecutor;
            }
            return new AsyncDifferConfig<>(
                    mMainThreadExecutor,
                    mBackgroundThreadExecutor,
                    mDiffCallback);
        }
    }
}
複製代碼

AsyncDifferConfig 對象中保存了兩個重要變量,mMainThreadExecutormBackgroundThreadExecutormBackgroundThreadExecutor 是一個最多容納兩個線程的線程池,用於異步執行 DiffUtil.calculateDiffmMainThreadExecutor 中實際真正執行的是 Handler 的調用。以下:

AsyncListDiffer.class -> MainThreadExecutor.class
private static class MainThreadExecutor implements Executor {
    final Handler mHandler = new Handler(Looper.getMainLooper());
    MainThreadExecutor() {}
    @Override
    public void execute(@NonNull Runnable command) {
        mHandler.post(command);
    }
}
複製代碼

建立 AsycnListDiffer 對象,首先聲明 DiffUtil.ItemCallback 對象:

//1. 聲明 DiffUtil.ItemCallback 回調
private val itemCallback = object : DiffUtil.ItemCallback<StudentBean>() {
    override fun areItemsTheSame(oldItem: StudentBean, newItem: StudentBean): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: StudentBean, newItem: StudentBean): Boolean {
        return oldItem.name == newItem.name && oldItem.age == newItem.age
    }
}
複製代碼

DiffUtil.ItemCallback 一樣具備 areItemTheSame()areContentsTheSame()getChangePayload() 方法,用法與 DiffUtil.Callback 相同。

接下來建立 AsycnListDiffer 對象:

//2. 建立 AsyncListDiff 對象
private val mDiffer = AsyncListDiffer<StudentBean>(this, itemCallback)
複製代碼

最後一步,更新列表數據:

fun submitList(studentList: List<StudentBean>) {
    //3. 提交新數據列表
    mDiffer.submitList(studentList)
}
複製代碼

AsycnListDiffer 的用法就是這麼簡單。總結其實就兩步:

  • 建立 AsyncListDiffer 對象。
  • 調用 submitList() 更新數據。

完整代碼以下:

class StudentAdapter(context: Context) : RecyclerView.Adapter<StudentAdapter.MyViewHolder>() {

    private val girlColor = "#FFD6E7"
    private val boyColor = "#BAE7FF"

    private val layoutInflater: LayoutInflater = LayoutInflater.from(context)

    //1. 聲明 DiffUtil.ItemCallback 回調
    private val itemCallback = object : DiffUtil.ItemCallback<StudentBean>() {
        override fun areItemsTheSame(oldItem: StudentBean, newItem: StudentBean): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: StudentBean, newItem: StudentBean): Boolean {
            return oldItem.name == newItem.name && oldItem.age == newItem.age
        }
    }

    //2. 建立 AsyncListDiff 對象
    private val mDiffer = AsyncListDiffer<StudentBean>(this, itemCallback)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentAdapter.MyViewHolder {
        return MyViewHolder(layoutInflater.inflate(R.layout.item_student, parent, false))
    }

    override fun getItemCount(): Int {
        return mDiffer.currentList.size
    }

    fun submitList(studentList: List<StudentBean>) {
        //3. 提交新數據列表
        mDiffer.submitList(studentList)
    }

    override fun onBindViewHolder(holder: StudentAdapter.MyViewHolder, position: Int) {
        //4. 重新數據列表中獲取最新數據
        val studentBean = mDiffer.currentList[position]
        when (studentBean.gender) {
            StudentBean.GENDER_GRIL -> {
                holder.rlRoot.setBackgroundColor(Color.parseColor(girlColor))
                holder.ivIcon.setBackgroundResource(R.mipmap.girl)
            }
            StudentBean.GENDER_BOY -> {
                holder.rlRoot.setBackgroundColor(Color.parseColor(boyColor))
                holder.ivIcon.setBackgroundResource(R.mipmap.boy)
            }
        }
        holder.tvName.text = studentBean.name
        holder.tvAge.text = studentBean.age.toString()
    }

    class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        val rlRoot: RelativeLayout = view.findViewById(R.id.rl_student_root)
        val ivIcon: ImageView = view.findViewById(R.id.iv_student_icon)
        val tvName: TextView = view.findViewById(R.id.tv_student_name)
        val tvAge: TextView = view.findViewById(R.id.tv_student_age)
    }
}
複製代碼

看下 submitList 中都作了什麼:

public void submitList(@Nullable final List<T> newList,
        @Nullable final Runnable commitCallback) {
    // 累計調用次數, 屢次執行 submitList() 僅生效最後一次調用
    final int runGeneration = ++mMaxScheduledGeneration;

    // 新\舊 數據集合對象相等時直接返回
    if (newList == mList) {
        // nothing to do (Note - still had to inc generation, since may have ongoing work)
        if (commitCallback != null) {
            commitCallback.run();
        }
        return;
    }

    final List<T> previousList = mReadOnlyList;

    // 新數據空, 全部 item 執行 remove 操做
    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;
    }

    // 第一次插入數據, 統一執行 inserted 操做
    if (mList == null) {
        mList = newList;
        mReadOnlyList = Collections.unmodifiableList(newList);
        // notify last, after list is updated
        mUpdateCallback.onInserted(0, newList.size());
        onCurrentListChanged(previousList, commitCallback);
        return;
    }

    final List<T> oldList = mList;
    // 異步執行 DiffUtil.calculateDiff 計算數據最小更新數
    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();
                }
            });

            //主線程中更新列表
            mMainThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    if (mMaxScheduledGeneration == runGeneration) {
                        latchList(newList, result, commitCallback);
                    }
                }
            });
        }
    });
}
複製代碼

最後貼一張圖:

下次遇到異步執行計算,根據計算結果主線程更新 UI 也能夠學習 AsyncListDiffer 寫一個工具類出來 ^_^

對你有用的話,留個讚唄^_^

相關文章
相關標籤/搜索