跳到主要内容

RecyclerView 深入

问题

RecyclerView 的缓存机制是怎样的?DiffUtil 是如何实现高效局部刷新的?

答案

1. RecyclerView 四大组件

2. 四级缓存机制

这是 RecyclerView 面试最高频考点:

层级名称容量是否需要 bind说明
第一级Scrap无限制屏幕内正在布局的 ViewHolder(notifyXxx 时临时缓存)
第二级CachedViews默认 2刚滑出屏幕的 ViewHolder,按 position 精确匹配
第三级ViewCacheExtension自定义自定义开发者自定义缓存层(很少使用)
第四级RecycledViewPool每种 type 5 个按 viewType 分组,需要重新绑定数据
面试要点
  • CachedViews 按 position 匹配,命中后无需重新 bind(性能最好)
  • RecycledViewPool 按 viewType 匹配,需要重新 bind
  • 多个 RecyclerView 可以共享一个 RecycledViewPool(如 ViewPager2 + RecyclerView 场景)

3. DiffUtil

DiffUtil 基于 Eugene W. Myers 差异算法,计算两个列表的最小编辑操作:

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

// 判断内容是否相同(决定是否需要局部刷新)
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem // data class 自动生成 equals
}

// 可选:返回具体变化的字段,用于 payload 局部刷新
override fun getChangePayload(oldItem: User, newItem: User): Any? {
val diff = mutableSetOf<String>()
if (oldItem.name != newItem.name) diff.add("name")
if (oldItem.avatar != newItem.avatar) diff.add("avatar")
return diff.ifEmpty { null }
}
}

4. ListAdapter(推荐用法)

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

class ViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(user: User) {
binding.textName.text = user.name
binding.textEmail.text = user.email
}

// payload 局部刷新
fun bindPayload(payloads: Set<String>, user: User) {
if ("name" in payloads) binding.textName.text = user.name
if ("avatar" in payloads) { /* 只更新头像 */ }
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}

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

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
@Suppress("UNCHECKED_CAST")
val changes = payloads.first() as Set<String>
holder.bindPayload(changes, getItem(position))
}
}
}

// 使用 - 只需提交新列表,DiffUtil 自动计算差异
adapter.submitList(newList)

5. 性能优化技巧

// 1. setHasFixedSize - Item 大小不变时避免 requestLayout
recyclerView.setHasFixedSize(true)

// 2. 预取 - LayoutManager 提前创建即将进入屏幕的 ViewHolder
(recyclerView.layoutManager as LinearLayoutManager).initialPrefetchItemCount = 4

// 3. 共享 RecycledViewPool
val sharedPool = RecyclerView.RecycledViewPool()
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)

// 4. 增大 CachedViews 容量
recyclerView.setItemViewCacheSize(4) // 默认 2

常见面试问题

Q1: RecyclerView 和 ListView 的区别?

答案

特性RecyclerViewListView
缓存层级四级缓存两级(ActiveViews、ScrapViews)
布局管理LayoutManager 灵活切换只支持垂直
局部刷新notifyItemChanged()只有 notifyDataSetChanged()
动画ItemAnimator无内置动画
ViewHolder强制使用非强制

RecyclerView 在架构设计上更灵活(组合模式),性能更优。

Q2: notifyDataSetChangedDiffUtil 有什么区别?

答案

  • notifyDataSetChanged():整体刷新,所有 ViewHolder 都会重新 bind,无动画效果
  • DiffUtil:计算最小差异,只刷新变化的 Item,有增删改动画。ListAdapter后台线程计算差异,不阻塞主线程

Q3: RecyclerView 滑动卡顿如何优化?

答案

  1. 减少 Item 布局层级:使用 ConstraintLayout 扁平化
  2. 图片优化:异步加载、合适尺寸、缓存
  3. 避免在 onBindViewHolder 中做耗时操作
  4. setHasFixedSize(true):Item 尺寸固定时
  5. 共享 RecycledViewPool:嵌套 RecyclerView 场景
  6. DiffUtil 替代全量刷新
  7. 预加载initialPrefetchItemCount

Q4: RecyclerView 的 Scrap 缓存在什么场景下使用?

答案

Scrap 缓存是在 scrollBynotifyXxx 时的临时缓存

  • AttachedScrapnotifyItemXxx 时没有发生变化的 ViewHolder
  • ChangedScrapnotifyItemChanged 时发生变化的 ViewHolder(需要重新 bind)

Scrap 缓存的生命周期很短,仅在一次 layout 过程中使用。

Q5: 什么是 ItemDecoration?它的 onDrawonDrawOver 有什么区别?

答案

ItemDecoration 用于在 Item 周围绘制装饰(分割线、间距、标签等):

  • onDraw():在 Item 下方绘制(会被 Item 遮住)
  • onDrawOver():在 Item 上方绘制(会遮住 Item)
  • getItemOffsets():设置 Item 的上下左右偏移,为装饰腾出空间

相关链接