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 的优化,核心就三点:
- 减少 onBindViewHolder 的耗时:把计算和 IO 操作移出去
- 使用 DiffUtil 或 ListAdapter:只更新变化的部分,避免全量刷新
- 用 ConcatAdapter 拆分复杂列表:每个模块独立维护,代码更清晰
嗯,这些技巧我在多个项目里验证过,效果都很不错。尤其是 ListAdapter,几乎成了我写列表的标配。你试试看,应该也会爱上它。