Skip to content

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)避免不必要的 requestLayoutRV 大小固定时
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 对比

维度RecyclerViewLazyColumn
范式命令式(Adapter + ViewHolder)声明式(items DSL)
复用ViewHolder 复用(四级缓存)Composition 复用(Slot Table)
布局LayoutManager内置(LazyColumn/LazyRow/LazyGrid)
动画ItemAnimatoranimateItemPlacement
性能成熟优化,略高持续改进中
开发效率较低(模板代码多)高(声明式简洁)
自定义灵活(自定义 LayoutManager)受限
kotlin
// Compose LazyColumn 等价写法
LazyColumn {
    items(
        items = users,
        key = { it.id },            // 等价于 stableId
        contentType = { "user" }    // 等价于 viewType,帮助复用
    ) { user ->
        UserItem(user)
    }
}