RecyclerView 详解(三)—— DiffUtil、性能优化与高级用法
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. DiffUtil 深入
1.1 Myers 差分算法
DiffUtil 使用 Eugene Myers 的差分算法,核心思想是在一个编辑图(edit graph)上找到从旧列表到新列表的最短编辑路径。
旧列表: [A, B, C, D]
新列表: [A, C, D, E]
最小编辑操作:
- 保留 A(位置不变)
- 删除 B
- 保留 C(位置从 2 移到 1)
- 保留 D(位置从 3 移到 2)
- 插入 E(位置 3)时间复杂度:O(N + D²),N 是新旧列表总长度,D 是编辑距离(差异数量)。列表越相似(D 越小),速度越快。
1.2 两个关键回调
kotlin
class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {
// 判断是否是同一个 item(通常比较 id)
// 返回 false → 认为是不同 item,触发 remove + add 动画
// 返回 true → 认为是同一个 item,继续调用 areContentsTheSame
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
// 判断同一个 item 的内容是否变化
// 返回 false → 触发 change 动画,调用 onBindViewHolder
// 返回 true → 什么都不做,完全复用
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem // data class 的 equals
}
// 可选:返回变化的部分(payload),实现局部更新
override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
val diff = mutableSetOf<String>()
if (oldItem.title != newItem.title) diff.add("title")
if (oldItem.avatar != newItem.avatar) diff.add("avatar")
return diff.ifEmpty { null }
}
}1.3 Payload 局部更新
Payload 是 DiffUtil 最强大的特性之一,可以只更新 item 中变化的部分,避免整个 ViewHolder 重新绑定:
kotlin
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
// 全量绑定
holder.bind(getItem(position))
}
// payloads 不为空时调用这个方法
override fun onBindViewHolder(holder: UserViewHolder, position: Int, payloads: List<Any>) {
if (payloads.isEmpty()) {
// 没有 payload,全量绑定
onBindViewHolder(holder, position)
return
}
// 有 payload,局部更新
val changes = payloads[0] as Set<String>
val item = getItem(position)
if ("title" in changes) {
holder.titleView.text = item.title // 只更新标题
}
if ("avatar" in changes) {
Glide.with(holder.avatarView).load(item.avatar).into(holder.avatarView) // 只更新头像
}
// 其他部分保持不变,不会闪烁
}
}好处:
- 避免整个 item 重新绑定导致的闪烁
- 图片不会重新加载(如果 URL 没变)
- 性能更好
1.4 ListAdapter vs AsyncListDiffer
kotlin
// ListAdapter(推荐,封装更完善)
class MyAdapter : ListAdapter<Item, VH>(diffCallback) {
// 提交新列表
fun update(list: List<Item>) {
submitList(list) // 内部自动异步 diff
}
}
// AsyncListDiffer(更灵活,可以在普通 Adapter 中使用)
class MyAdapter : RecyclerView.Adapter<VH>() {
private val differ = AsyncListDiffer(this, diffCallback)
override fun getItemCount() = differ.currentList.size
fun update(list: List<Item>) {
differ.submitList(list)
}
}注意事项:
submitList传入的必须是新的 List 实例,传同一个引用不会触发 diff- 连续快速调用
submitList,中间的会被跳过,只执行最后一次 - diff 计算在后台线程,结果分发在主线程
kotlin
// ❌ 错误:修改同一个 list 再提交,diff 认为没变化
val list = mutableListOf(item1, item2)
adapter.submitList(list)
list.add(item3)
adapter.submitList(list) // 同一个引用,不会更新!
// ✅ 正确:提交新的 list
adapter.submitList(list.toList()) // 创建新的 List 实例2. 性能优化详解
2.1 setHasFixedSize(true)
java
// RecyclerView 源码
void onItemRangeInserted(int positionStart, int itemCount) {
if (mHasFixedSize) {
// 只触发 item 级别的布局
mLayout.onItemsAdded(this, positionStart, itemCount);
} else {
// 触发整个 RecyclerView 的 requestLayout
requestLayout();
}
}当 item 的增删不会改变 RecyclerView 自身大小时(比如 RecyclerView 是 match_parent),设置 setHasFixedSize(true) 可以避免不必要的 requestLayout,减少 measure 开销。
2.2 Prefetch 预取机制
RecyclerView 25.1.0 引入的 GapWorker 预取机制:
正常滑动时:
帧 1: [滑动处理 + fill] ─────────── [空闲]
帧 2: [滑动处理 + fill] ─────────── [空闲]
开启预取后:
帧 1: [滑动处理 + fill] [预取下一个 item] [空闲]
帧 2: [滑动处理 + fill(命中预取缓存)] ── [空闲]GapWorker 利用每帧的空闲时间,提前创建和绑定即将出现的 ViewHolder。预取的 ViewHolder 存放在 mCachedViews 中。
java
// GapWorker 核心逻辑
void prefetch(long deadlineNs) {
// 根据滑动速度预测下一个需要的 position
int position = layoutManager.collectAdjacentPrefetchPositions();
// 在 deadline 之前创建和绑定
RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
position, deadlineNs);
}嵌套 RecyclerView 的预取优化:
kotlin
// 外层 RecyclerView 的 LayoutManager 设置内层预取数量
(recyclerView.layoutManager as LinearLayoutManager).apply {
// 告诉外层 LayoutManager,内层 RecyclerView 需要预取的 item 数量
initialPrefetchItemCount = 4 // 内层一屏可见的 item 数
}2.3 共享 RecycledViewPool
多个 RecyclerView 展示相同类型的 item 时(如 ViewPager + RecyclerView),共享 Pool 可以避免重复创建 ViewHolder:
kotlin
// 创建共享 Pool
val sharedPool = RecyclerView.RecycledViewPool().apply {
setMaxRecycledViews(TYPE_NORMAL, 20) // 增大容量
}
// ViewPager 的每个页面的 RecyclerView 共享同一个 Pool
class PageAdapter : RecyclerView.Adapter<VH>() {
override fun onBindViewHolder(holder: VH, position: Int) {
val innerRecyclerView = holder.recyclerView
innerRecyclerView.setRecycledViewPool(sharedPool)
}
}2.4 onBindViewHolder 优化
kotlin
// ❌ 在 onBindViewHolder 中设置点击监听(每次 bind 都创建新 lambda)
override fun onBindViewHolder(holder: VH, position: Int) {
holder.itemView.setOnClickListener {
onClick(getItem(position)) // 每次 bind 创建新的 lambda
}
}
// ✅ 在 onCreateViewHolder 中设置(只创建一次)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
val holder = VH(view)
holder.itemView.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
onClick(getItem(pos))
}
}
return holder
}kotlin
// ❌ 在 onBindViewHolder 中创建对象
override fun onBindViewHolder(holder: VH, position: Int) {
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) // 每次创建!
holder.dateView.text = formatter.format(item.date)
}
// ✅ 复用对象
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
override fun onBindViewHolder(holder: VH, position: Int) {
holder.dateView.text = dateFormatter.format(item.date)
}2.5 图片加载优化
kotlin
override fun onBindViewHolder(holder: VH, position: Int) {
// ✅ 使用 Glide 自动管理生命周期
Glide.with(holder.itemView)
.load(item.imageUrl)
.placeholder(R.drawable.placeholder) // 占位图,避免闪烁
.override(200, 200) // 指定目标尺寸,避免加载原图
.into(holder.imageView)
}
// ✅ 滑动时暂停加载,停止时恢复
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> Glide.with(context).pauseRequests()
RecyclerView.SCROLL_STATE_IDLE -> Glide.with(context).resumeRequests()
}
}
})2.6 性能优化清单
| 优化项 | 效果 | 适用场景 |
|---|---|---|
| setHasFixedSize(true) | 避免不必要的 requestLayout | RV 大小固定时 |
| DiffUtil / ListAdapter | 精确更新,有动画 | 替代 notifyDataSetChanged |
| Payload 局部更新 | 避免整个 item 重绑 | item 部分内容变化 |
| setItemViewCacheSize | 增大离屏缓存 | 频繁来回滑动 |
| 共享 RecycledViewPool | 减少 createViewHolder | 多个 RV 相同 viewType |
| Prefetch 预取 | 利用空闲时间提前创建 | 默认开启 |
| onCreateViewHolder 设置监听 | 减少对象创建 | 所有场景 |
| 避免 onBind 创建对象 | 减少内存抖动 | 所有场景 |
| 图片指定尺寸 | 减少内存和解码时间 | 有图片的列表 |
| RecyclerView.setItemAnimator(null) | 去掉动画开销 | 不需要动画时 |
3. 高级用法
3.1 ConcatAdapter(合并多个 Adapter)
kotlin
// 替代多 viewType 的复杂 Adapter
val headerAdapter = HeaderAdapter()
val contentAdapter = ContentAdapter()
val footerAdapter = FooterAdapter()
recyclerView.adapter = ConcatAdapter(
ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true) // 不同 Adapter 的 viewType 互相隔离
.build(),
headerAdapter,
contentAdapter,
footerAdapter
)
// 各自独立管理数据
headerAdapter.submitHeader(header)
contentAdapter.submitList(items)
footerAdapter.showLoading(true)优势:
- 每个 Adapter 职责单一,易维护
- 不需要在一个 Adapter 中处理多种 viewType
- 各 Adapter 独立更新,互不影响
3.2 ItemTouchHelper(拖拽和滑动删除)
kotlin
val callback = object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, // 拖拽方向
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // 滑动方向
) {
override fun onMove(rv: RecyclerView, source: ViewHolder, target: ViewHolder): Boolean {
val from = source.bindingAdapterPosition
val to = target.bindingAdapterPosition
// 交换数据
Collections.swap(list, from, to)
adapter.notifyItemMoved(from, to)
return true
}
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
val pos = viewHolder.bindingAdapterPosition
// 删除数据
list.removeAt(pos)
adapter.notifyItemRemoved(pos)
}
// 自定义滑动时的绘制效果
override fun onChildDraw(c: Canvas, rv: RecyclerView, viewHolder: ViewHolder,
dX: Float, dY: Float, actionState: Int, isActive: Boolean) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
// 滑动时绘制红色背景和删除图标
val background = ColorDrawable(Color.RED)
background.setBounds(viewHolder.itemView.right + dX.toInt(), ...)
background.draw(c)
}
super.onChildDraw(c, rv, viewHolder, dX, dY, actionState, isActive)
}
}
ItemTouchHelper(callback).attachToRecyclerView(recyclerView)3.3 RecyclerView 嵌套优化
RecyclerView 嵌套 RecyclerView(如横向列表嵌在纵向列表中)的优化要点:
kotlin
// 1. 共享 RecycledViewPool
val sharedPool = RecyclerView.RecycledViewPool()
override fun onBindViewHolder(holder: OuterVH, position: Int) {
holder.innerRecyclerView.apply {
setRecycledViewPool(sharedPool)
// 2. 设置预取数量
(layoutManager as LinearLayoutManager).initialPrefetchItemCount = 4
// 3. 设置固定大小
setHasFixedSize(true)
// 4. 避免重复设置 LayoutManager
if (layoutManager == null) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
// 5. 避免重复设置 Adapter(复用时只更新数据)
if (adapter == null) {
adapter = InnerAdapter()
}
(adapter as InnerAdapter).submitList(data[position].innerList)
}
}3.4 保存和恢复滚动位置
kotlin
// 保存
val state = recyclerView.layoutManager?.onSaveInstanceState()
// 恢复
recyclerView.layoutManager?.onRestoreInstanceState(state)
// 或者在 Activity/Fragment 中自动保存恢复
// RecyclerView 会自动保存滚动位置(前提是有 id)3.5 RecyclerView 与 Compose LazyColumn 对比
| 维度 | RecyclerView | LazyColumn |
|---|---|---|
| 范式 | 命令式(Adapter + ViewHolder) | 声明式(items DSL) |
| 复用 | ViewHolder 复用(四级缓存) | Composition 复用(Slot Table) |
| 布局 | LayoutManager | 内置(LazyColumn/LazyRow/LazyGrid) |
| 动画 | ItemAnimator | animateItemPlacement |
| 性能 | 成熟优化,略高 | 持续改进中 |
| 开发效率 | 较低(模板代码多) | 高(声明式简洁) |
| 自定义 | 灵活(自定义 LayoutManager) | 受限 |
kotlin
// Compose LazyColumn 等价写法
LazyColumn {
items(
items = users,
key = { it.id }, // 等价于 stableId
contentType = { "user" } // 等价于 viewType,帮助复用
) { user ->
UserItem(user)
}
}