更高效地刷新 RecyclerView | DiffUtil二次封装

简介: 每次数据变化都全量刷新整个列表是很奢侈的,不仅整个列表会闪烁一下,而且所有可见表项都会重新绑定一遍数据。这一篇对 DiffUtil 进行二次封装以让其更易于使用。

每次数据变化都全量刷新整个列表是很奢侈的,不仅整个列表会闪烁一下,而且所有可见表项都会重新执行一遍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()释放协程资源。

无类型适配器持有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)

Talk is cheap, show me the code

预告

下一篇会介绍一种超简便的RecyclerView预加载方法

推荐阅读

RecyclerView 系列文章目录如下:

  1. RecyclerView 缓存机制 | 如何复用表项?
  2. RecyclerView 缓存机制 | 回收些什么?
  3. RecyclerView 缓存机制 | 回收到哪去?
  4. RecyclerView缓存机制 | scrap view 的生命周期
  5. 读源码长知识 | 更好的RecyclerView点击监听器
  6. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
  7. 更好的 RecyclerView 表项子控件点击监听器
  8. 更高效地刷新 RecyclerView | DiffUtil二次封装
  9. 换一个思路,超简单的RecyclerView预加载
  10. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
  11. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
  12. RecyclerView 动画原理 | 如何存储并应用动画属性值?
  13. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
  14. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
  15. RecyclerView 性能优化 | 把加载表项耗时减半 (一)
  16. RecyclerView 性能优化 | 把加载表项耗时减半 (二)
  17. RecyclerView 性能优化 | 把加载表项耗时减半 (三)
  18. RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
  19. RecyclerView 的滚动时怎么实现的?(二)| Fling
  20. RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?
目录
相关文章
|
人工智能 计算机视觉
Photoshop2023新版本win11系统安装下载教程
ps迎来了2023的版本,这次的版本提升针对windows11做了特别优化,启动速度比win10快了很多。期盼已久的Win版 PS 2023 终于来了,小编也是通过特殊渠道搞来的,本期带来的WIN版本支持一键安装激活,一次安装永久免费使用众所周知,版本越高,需要的电脑配置也就越来越高。下面放一下2023版本的配置供大家参考。需要注意的是这些版本不再支持windows7系统,仅支持win10及以上的操作系统。
2298 0
|
12月前
|
Android开发 开发者 索引
Android实战经验之如何使用DiffUtil提升RecyclerView的刷新性能
本文介绍如何使用 `DiffUtil` 实现 `RecyclerView` 数据集的高效更新,避免不必要的全局刷新,尤其适用于处理大量数据场景。通过定义 `DiffUtil.Callback`、计算差异并应用到适配器,可以显著提升性能。同时,文章还列举了常见错误及原因,帮助开发者避免陷阱。
853 9
|
ARouter 前端开发 Java
MVVM架构结合阿里ARouter,打造一套Android-Databinding组件化
前言 关于Android的组件化,相信大家并不陌生,网上谈论组件化的文章,多如过江之鲫,然而一篇基于MVVM模式的组件化方案却很少。
2128 3
|
9月前
|
人工智能 IDE 程序员
GitHub Copilot 免费了!程序员们的福音来了!
《GitHub Copilot 免费了!程序员们的福音来了!》 近日,GitHub 宣布其 AI 编程助手 GitHub Copilot 现在可以免费使用。曾经每月需支付 10 美元订阅费的 Copilot,现在向所有人开放免费版本,这对个人开发者、初学者和小型团队来说是个大好消息。免费版支持 GPT 和 Claude 模型,并提供每月 2000 次代码补全和 50 条聊天消息等核心功能。用户只需注册或登录 GitHub 账户,在 VS Code 中安装扩展并激活免费版即可使用。此外,Visual Studio Code 也完全免费,进一步降低了开发门槛。 除了
10768 7
GitHub Copilot 免费了!程序员们的福音来了!
|
10月前
|
安全 网络安全 数据安全/隐私保护
政务内网实现https访问教程
政务内网实现HTTPS访问需经过多个步骤:了解HTTPS原理,选择并申请适合的SSL证书,配置SSL证书至服务器,设置端口映射与访问控制,测试验证HTTPS访问功能,注意证书安全性和兼容性,定期备份与恢复。这些措施确保了数据传输的安全性,提升了政务服务的效率与安全性。
|
10月前
|
机器学习/深度学习 数据采集 算法框架/工具
使用Python实现智能生态系统监测与保护的深度学习模型
使用Python实现智能生态系统监测与保护的深度学习模型
395 4
|
测试技术 uml UED
软件需求管理:从获取到变更的全过程
【8月更文第20天】在软件开发项目中,需求管理是确保产品满足用户期望和业务目标的关键环节。本文将探讨软件需求管理的基本概念、需求获取的方法、需求分析与建模的实践、需求验证与确认的策略以及需求变更管理的最佳实践。
1046 5
|
存储 并行计算 Ubuntu
Nvidia Jetson Orin系列配置教程
本文是Nvidia Jetson Orin系列的配置教程,介绍了两种安装方法:通过Nvidia SDK Manager进行安装和通过本地镜像烧录进行安装。第一种方法包括下载SDK Manager、安装和使用工具进行Jetson系列硬件的配置。第二种方法包括下载官方镜像、使用Etcher烧录镜像、安装镜像、安装开发环境以及检查开发环境是否配置成功。文中还提供了CUDA、cuDNN、TensorRT和OpenCV的检查命令和预期结果。
2482 0
Nvidia Jetson Orin系列配置教程
|
存储 监控 数据库
Android经典实战之OkDownload的文件分段下载及合成原理
本文介绍了 OkDownload,一个高效的 Android 下载引擎,支持多线程下载、断点续传等功能。文章详细描述了文件分段下载及合成原理,包括任务创建、断点续传、并行下载等步骤,并展示了如何通过多种机制保证下载的稳定性和完整性。
434 0
|
SQL 关系型数据库 MySQL
【MySQL】:探秘主流关系型数据库管理系统及SQL语言
【MySQL】:探秘主流关系型数据库管理系统及SQL语言
612 0

热门文章

最新文章