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 的区别?
答案:
| 特性 | RecyclerView | ListView |
|---|---|---|
| 缓存层级 | 四级缓存 | 两级(ActiveViews、ScrapViews) |
| 布局管理 | LayoutManager 灵活切换 | 只支持垂直 |
| 局部刷新 | notifyItemChanged() | 只有 notifyDataSetChanged() |
| 动画 | ItemAnimator | 无内置动画 |
| ViewHolder | 强制使用 | 非强制 |
RecyclerView 在架构设计上更灵活(组合模式),性能更优。
Q2: notifyDataSetChanged 和 DiffUtil 有什么区别?
答案:
notifyDataSetChanged():整体刷新,所有 ViewHolder 都会重新 bind,无动画效果DiffUtil:计算最小差异,只刷新变化的 Item,有增删改动画。ListAdapter在后台线程计算差异,不阻塞主线程
Q3: RecyclerView 滑动卡顿如何优化?
答案:
- 减少 Item 布局层级:使用 ConstraintLayout 扁平化
- 图片优化:异步加载、合适尺寸、缓存
- 避免在
onBindViewHolder中做耗时操作 setHasFixedSize(true):Item 尺寸固定时- 共享 RecycledViewPool:嵌套 RecyclerView 场景
DiffUtil替代全量刷新- 预加载:
initialPrefetchItemCount
Q4: RecyclerView 的 Scrap 缓存在什么场景下使用?
答案:
Scrap 缓存是在 scrollBy 或 notifyXxx 时的临时缓存:
- AttachedScrap:
notifyItemXxx时没有发生变化的 ViewHolder - ChangedScrap:
notifyItemChanged时发生变化的 ViewHolder(需要重新 bind)
Scrap 缓存的生命周期很短,仅在一次 layout 过程中使用。
Q5: 什么是 ItemDecoration?它的 onDraw 和 onDrawOver 有什么区别?
答案:
ItemDecoration 用于在 Item 周围绘制装饰(分割线、间距、标签等):
onDraw():在 Item 下方绘制(会被 Item 遮住)onDrawOver():在 Item 上方绘制(会遮住 Item)getItemOffsets():设置 Item 的上下左右偏移,为装饰腾出空间