美文网首页Android进阶之路Android开发Android开发经验谈
更高效地刷新 RecyclerView | DiffUtil二次

更高效地刷新 RecyclerView | DiffUtil二次

作者: Android开发指南 | 来源:发表于2020-10-17 22:27 被阅读0次

    每次数据变化都全量刷新整个列表是很奢侈的,不仅整个列表会闪烁一下,而且所有可见表项都会重新执行一遍onBindViewHolder()并重绘列表(即便它并不需要刷新)。若表项视图复杂,会显著影响列表性能。

    更高效的刷新方式应该是:只刷新数据发生变化的表项。RecyclerView.Adapter有 4 个非全量刷新方法,分别是:notifyItemRangeInserted()notifyItemRangeChanged()notifyItemRangeRemovednotifyItemMoved()。调用它们时都需指定变化范围,这要求业务层了解数据变化的细节,无疑增加了调用难度。

    DiffUtil模版代码

    androidx.recyclerview.widget包下有一个工具类叫DiffUtil,它利用了一种算法计算出两个列表间差异,并且可以直接应用到RecyclerView.Adapter上,自动实现非全量刷新。

    使用DiffUtil的模版代码如下:

    val oldList = ... // 老列表
    val newList = ... // 新列表
    val adapter:RecyclerView.Adapter = ...
    
    // 1.定义比对方法
    val callback = object : DiffUtil.Callback() {
        override fun getOldListSize(): Int = oldList.size
        override fun getNewListSize(): Int = newList.size
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            // 分别获取新老列表中对应位置的元素
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return ... // 定义什么情况下新老元素是同一个对象(通常是业务id)
        }
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return ... // 定义什么情况下同一对象内容是否相同 (由业务逻辑决定)
        }
        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return ... // 具体定义同一对象内容是如何地不同 (返回值会作为payloads传入onBindViewHoder())
        }
    }
    // 2.进行比对并输出结果
    val diffResult = DiffUtil.calculateDiff(callback)
    // 3\. 将比对结果应用到 adapter
    diffResult.dispatchUpdatesTo(adapter)
    复制代码
    

    DiffUtil需要 3 个输入,一个老列表,一个新列表,一个DiffUtil.Callback,其中的Callback的实现和业务逻辑有关,它定义了如何比对列表中的数据。

    判定列表中数据是否相同分为递进三个层次:

    1. 是否是同一个数据:对应areItemsTheSame()
    2. 若是同一个数据,其中具体内容是否相同:对应areContentsTheSame()(当areItemsTheSame()返回true时才会被调用)
    3. 若同一数据的具体内容不同,则找出不同点:对应getChangePayload()(当areContentsTheSame()返回false时才会被调用)

    DiffUtil输出 1 个比对结果DiffResult,该结果可以应用到RecyclerView.Adapter上:

    // 将比对结果应用到Adapter
    public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
        dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
    }
    
    // 将比对结果应用到ListUpdateCallback
    public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {...}
    
    // 基于 RecyclerView.Adapter 实现的列表更新回调
    public final class AdapterListUpdateCallback implements ListUpdateCallback {
        private final RecyclerView.Adapter mAdapter;
        public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
            mAdapter = adapter;
        }
        @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将比对结果以ListUpdateCallback回调的形式反馈给业务层。插入、移除、移动、更新这四个回调表示列表内容四种可能的变化,对于RecyclerView.Adapter来说正好对应着四个非全量更新方法。

    DiffUtil.Callback与业务解耦

    不同的业务场景,需要实现不同的DiffUtil.Callback,因为它和具体的业务数据耦合。这使得它无法和上一篇介绍的类型无关适配器一起使用。

    有没有办法可以使 DiffUtil.Callback的实现和具体业务数据解耦?

    这里的业务逻辑是“比较数据是否一致”的算法,是不是可以把这段逻辑写在数据类体内?

    拟定了一个新接口:

    interface Diff {
        // 判断当前对象和给定对象是否是同一对象
        fun isSameObject(other: Any): Boolean
        // 判断当前对象和给定对象是否拥有相同内容 
        fun hasSameContent(other: Any): Boolean
        // 返回当前对象和给定对象的差异
        fun diff(other: Any): Any
    }
    复制代码
    

    然后让数据类实现该接口:

    data class Text(
        var text: String,
        var type: Int,
        var id: Int
    ) : Diff {
        override fun isSameObject(other: Any): Boolean = this.id == other.id
        override fun hasSameContent(other: Any): Boolean = this.text == other.text
        override fun diff(other: Any?): Any? {
            return when {
                other !is Text -> null
                this.text != other.text -> {"text change"}
                else -> null
            }
        }
    }
    复制代码
    

    这样DiffUtil.Callback的逻辑就可以和业务数据解耦:

    // 包含任何数据类型的列表
    val newList: List<Any> = ... 
    val oldList: List<Any> = ...
    val callback = object : DiffUtil.Callback() {
        override fun getOldListSize(): Int = oldList.size
        override fun getNewListSize(): Int = newList.size
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            // 将数据强转为Diff
            val oldItem = oldList[oldItemPosition] as? Diff
            val newItem = newList[newItemPosition] as? Diff
            if (oldItem == null || newItem == null) return false
            return oldItem.isSameObject(newItem)
        }
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition] as? Diff
            val newItem = newList[newItemPosition] as? Diff
            f (oldItem == null || newItem == null) return false
            return oldItem.hasSameContent(newItem)
        }
        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            val oldItem = oldList[oldItemPosition] as? Diff
            val newItem = newList[newItemPosition] as? Diff
            if (oldItem == null || newItem == null) return null
            return oldItem.diff(newItem)
        }
    }
    复制代码
    

    转念一想,所有非空类的基类Any中就包含了这些语义:

    public open class Any {
        // 用于判断当前对象和另一个对象是否是同一个对象
        public open operator fun equals(other: Any?): Boolean
        // 返回当前对象哈希值
        public open fun hashCode(): Int
    }
    复制代码
    

    这样就可以简化Diff接口:

    interface Diff {
        infix fun diff(other: Any?): Any?
    }
    复制代码
    

    保留字infix表示这个函数的调用可以使用中缀表达式,以增加代码可读性(效果见下段代码),关于它的详细介绍可以点击这里

    数据实体类和DiffUtil.Callback的实现也被简化:

    data class Text(
        var text: String,
        var type: Int,
        var id: Int
    ) : Diff {
        override fun hashCode(): Int = this.id
        override fun diff(other: Any?): Any? {
            return when {
                other !is Text -> null
                this.text != other.text -> {"text diff"}
                else -> null
            }
        }
        override fun equals(other: Any?): Boolean {
            return (other as? Text)?.text == this.text
        }
    }
    
    val callback = object : DiffUtil.Callback() {
        override fun getOldListSize(): Int = oldList.size
        override fun getNewListSize(): Int = newList.size
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem.hashCode() == newItem.hashCode()
        }
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem == newItem
        }
        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            val oldItem = oldList[oldItemPosition] as? Diff
            val newItem = newList[newItemPosition] as? Diff
            if (oldItem == null || newItem == null) return null
            return oldItem diff newItem // 中缀表达式
        }
    }
    复制代码
    

    DiffUtil.calculateDiff()异步化

    比对算法是耗时的,将其异步化是稳妥的。

    androidx.recyclerview.widget包下已经有一个可直接使用的AsyncListDiffer

    // 使用时必须指定一个具体的数据类型
    public class AsyncListDiffer<T> {
        // 执行比对的后台线程
        Executor mMainThreadExecutor;
        // 用于将比对结果抛到主线程
        private static class MainThreadExecutor implements Executor {
            final Handler mHandler = new Handler(Looper.getMainLooper());
            MainThreadExecutor() {}
            @Override
            public void execute(@NonNull Runnable command) {
                mHandler.post(command);
            }
        }
        // 提交新列表数据
        public void submitList(@Nullable final List<T> newList){
            // 在后台执行比对...
        }
        ...
    }
    复制代码
    

    它在后台线程执行比对,并将结果抛到主线程。可惜的是它和类型绑定,无法和无类型适配器一起使用。

    无奈只能参考它的思想重新写一个自己的:

    class AsyncListDiffer(
        // 之所以使用listUpdateCallback,目的是让AsyncListDiffer的适用范围不局限于RecyclerView.Adapter
        var listUpdateCallback: ListUpdateCallback,
        // 自定义协程的调度器,用于适配既有代码,把比对逻辑放到既有线程中,而不是新起一个
        dispatcher: CoroutineDispatcher 
    ) : DiffUtil.Callback(), CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher) {
        // 可装填任何类型的新旧列表
        var oldList = listOf<Any>()
        var newList = listOf<Any>()
        // 用于标记每一次提交列表
        private var maxSubmitGeneration: Int = 0
        // 提交新列表
        fun submitList(newList: List<Any>) {
            val submitGeneration = ++maxSubmitGeneration
            this.newList = newList
            // 快速返回:没有需要更新的东西
            if (this.oldList == newList) return
            // 快速返回:旧列表为空,全量接收新列表
            if (this.oldList.isEmpty()) {
                this.oldList = newList
                // 保存列表最新数据的快照
                oldList = newList.toList()
                listUpdateCallback.onInserted(0, newList.size)
                return
            }
            // 启动协程比对数据
            launch {
                val diffResult = DiffUtil.calculateDiff(this@AsyncListDiffer)
                // 保存列表最新数据的快照
                oldList = newList.toList()
                // 将比对结果抛到主线程并应用到ListUpdateCallback接口
                withContext(Dispatchers.Main) {
                    // 只保留最后一次提交的比对结果,其他的都被丢弃
                    if (submitGeneration == maxSubmitGeneration) {
                        diffResult.dispatchUpdatesTo(listUpdateCallback)
                    }
                }
            }
        }
    
        override fun getOldListSize(): Int = oldList.size
        override fun getNewListSize(): Int = newList.size
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem.hashCode() == newItem.hashCode()
        }
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem == newItem
        }
        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            val oldItem = oldList[oldItemPosition] as? Diff
            val newItem = newList[newItemPosition] as? Diff
            if (oldItem == null || newItem == null) return null
            return oldItem diff newItem
        }
    }
    复制代码
    

    AsyncListDiffer实现了DiffUtil.CallbackCoroutineScope接口,并且将后者的实现委托给了CoroutineScope(SupervisorJob() + dispatcher)实例,这样做的好处是在AsyncListDiffer内部任何地方可以无障碍地启动协程,而在外部可以通过AsyncListDiffer的实例调用cancel()释放协程资源。

    其中关于类委托的详细讲解可以点击Kotlin实战 | 2 = 12 ?泛型、类委托、重载运算符综合应用,关于协程的详细讲解可以点击Kotlin 基础 | 为什么要这样用协程?

    无类型适配器持有AsyncListDiffer就大功告成了:

    class VarietyAdapter(
        private var proxyList: MutableList<Proxy<*, *>> = mutableListOf(),
        dispatcher: CoroutineDispatcher = Dispatchers.IO // 默认在IO共享线程池中执行比对
    ) : RecyclerView.Adapter<ViewHolder>() {
        // 构建数据比对器
        private val dataDiffer = AsyncListDiffer(AdapterListUpdateCallback(this), dispatcher)
        // 业务代码通过为dataList赋值实现填充数据
        var dataList: List<Any>
            set(value) {
                // 将填充数据委托给数据比对器
                dataDiffer.submitList(value)
            }
            // 返回上一次比对后的数据快照
            get() = dataDiffer.oldList
    
            override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
                dataDiffer.cancel() // 当适配器脱离RecyclerView时释放协程资源
        }
        ...
    }
    复制代码
    

    只列出了VarietyAdapterAsyncListDiffer相关的部分,它的详细讲解可以点击代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂

    然后就可以像这样使用:

    var itemNumber = 1
    // 构建适配器
    val varietyAdapter = VarietyAdapter().apply {
        // 为列表新增两种数据类型
        addProxy(TextProxy())
        addProxy(ImageProxy())
        // 初始数据集(包含两种不同的数据)
        dataList = listOf(
            Text("item ${itemNumber++}"),
            Image("#00ff00"),
            Text("item ${itemNumber++}"),
            Text("item ${itemNumber++}"),
            Image("#88ff00"),
            Text("item ${itemNumber++}")
        )
        // 预加载(上拉列表时预加载下一屏内容)
        onPreload = {
            // 获取老列表快照(深拷贝)
            val oldList = dataList
            // 在老列表快照尾部添加新内容
            dataList = oldList.toMutableList().apply {
                addAll(
                    listOf(
                        Text("item ${itemNumber++}", 2),
                        Text("item ${itemNumber++}", 2),
                        Text("item ${itemNumber++}", 2),
                        Text("item ${itemNumber++}", 2),
                        Text("item ${itemNumber++}", 2),
                    )
                )
            }
        }
    }
    // 应用适配器
    recyclerView?.adapter = varietyAdapter
    recyclerView?.layoutManager = LinearLayoutManager(this)
    

    作者:唐子玄
    链接:https://juejin.im/post/6882531923537707015

    相关文章

      网友评论

        本文标题:更高效地刷新 RecyclerView | DiffUtil二次

        本文链接:https://www.haomeiwen.com/subject/yqxqmktx.html