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 写一个工具类出来 ^_^

对你有用的话,留个赞呗^_^

相关文章
相关标签/搜索