23、RecyclerView:RecyclerView优化、DiffUtil与ListAdapter、ConcatAdapter

RecyclerView 是 Android 开发中最常用的列表组件,没有之一。但说实话,很多人用了好几年,也只是停留在「会用」的阶段。性能一塌糊涂,列表一卡一卡的,还找不到原因。

今天我们就来聊聊 RecyclerView 的优化,以及两个非常实用的工具:DiffUtil 和 ListAdapter。最后再介绍一个我最近特别喜欢用的 ConcatAdapter。

RecyclerView 的常见性能问题

先说说我踩过的坑。有一次做 IM 聊天列表,消息一多,滑动就掉帧。排查了半天,发现是 onBindViewHolder 里做了太多耗时操作。

RecyclerView 的性能瓶颈,说白了就几个地方:

  • onBindViewHolder 太慢:每次绑定数据都做大量计算或 IO 操作
  • 布局嵌套太深:一个 item 里套了五六层 LinearLayout
  • 频繁 notifyDataSetChanged:整个列表全部重绘,浪费资源
  • 图片加载没做缓存:每次滑动都重新加载图片

核心原则:onBindViewHolder 里只做数据绑定,不做耗时操作。复杂计算放到后台线程,图片用 Glide 或 Coil 加载。

DiffUtil:智能计算列表差异

DiffUtil 是 Support Library 提供的一个工具类。它的作用很简单:比较新旧两个列表,找出最小差异

你想想看,以前我们更新列表怎么做?要么 notifyDataSetChanged,要么自己写一堆 if-else 判断。前者性能差,后者容易出错。

DiffUtil 帮我们做了这件事。它会计算出:哪些 item 是新增的、哪些是删除的、哪些是移动的、哪些只是内容变了。

class MyDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        // 判断是否是同一个 item(通常用 id)
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        // 判断内容是否相同
        return oldItem == newItem
    }
}

使用的时候也很简单:

val diffResult = DiffUtil.calculateDiff(MyDiffCallback(oldList, newList))
diffResult.dispatchUpdatesTo(adapter)

小技巧:如果列表数据量很大(超过 1000 条),建议把 DiffUtil 的计算放到后台线程。我一般用 coroutine 的 withContext(Dispatchers.Default) 来做。

ListAdapter:更优雅的列表更新

ListAdapter 是 DiffUtil 的封装版。它把 DiffUtil 的计算和分发过程都封装好了,我们只需要实现回调方法就行。

我个人习惯用 ListAdapter 替代传统的 RecyclerView.Adapter。原因很简单:代码更少,更不容易出错

class UserAdapter : ListAdapter<User, UserAdapter.ViewHolder>(MyDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_user, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val user = getItem(position)
        holder.bind(user)
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(user: User) {
            itemView.name.text = user.name
            itemView.age.text = user.age.toString()
        }
    }
}

更新数据的时候,只需要调用 submitList() 方法:

adapter.submitList(newUserList)

ListAdapter 会自动在后台线程计算差异,然后只更新变化的部分。滑动性能提升非常明显。

注意:submitList() 会异步执行 diff 计算。如果你在短时间内多次调用 submitList(),只有最后一次会生效。这是设计如此,不是 bug。

ConcatAdapter:组合多个 Adapter

ConcatAdapter 是 RecyclerView 1.2.0 引入的新功能。它的作用是把多个 Adapter 串联起来,显示在同一个 RecyclerView 里。

以前要实现类似效果,要么用多个 ViewType,要么用 SectionAdapter。前者代码混乱,后者需要额外依赖。

ConcatAdapter 的思路很直接:把多个 Adapter 当成「片段」,按顺序拼接

val headerAdapter = HeaderAdapter()
val contentAdapter = ContentAdapter()
val footerAdapter = FooterAdapter()

val concatAdapter = ConcatAdapter(headerAdapter, contentAdapter, footerAdapter)
recyclerView.adapter = concatAdapter

每个 Adapter 管理自己的数据,互不干扰。headerAdapter 更新数据时,只影响头部区域,不会触发 contentAdapter 的重绘。

我在项目中遇到过这样一个场景:一个商品详情页,顶部是轮播图,中间是商品信息,底部是推荐列表。以前用 ViewType 写了一大堆 if-else,维护起来很痛苦。换成 ConcatAdapter 后,每个模块独立成 Adapter,代码清晰多了。

SVG 流程图:RecyclerView 优化核心逻辑

RecyclerView 优化核心逻辑 RecyclerView 列表 性能优化 DiffUtil / ListAdapter ConcatAdapter onBindViewHolder 轻量化 布局扁平化 图片缓存 areItemsTheSame areContentsTheSame submitList 异步更新 多 Adapter 拼接 独立数据管理 避免 ViewType 混乱

总结

RecyclerView 的优化,核心就三点:

  1. 减少 onBindViewHolder 的耗时:把计算和 IO 操作移出去
  2. 使用 DiffUtil 或 ListAdapter:只更新变化的部分,避免全量刷新
  3. 用 ConcatAdapter 拆分复杂列表:每个模块独立维护,代码更清晰

嗯,这些技巧我在多个项目里验证过,效果都很不错。尤其是 ListAdapter,几乎成了我写列表的标配。你试试看,应该也会爱上它。

公众号:蓝海资料掘金营,微信 deep3321