跳到主要内容

列表滑动卡顿优化

场景

RecyclerView 列表在快速滑动时出现明显掉帧(FPS < 50),用户体感卡顿。

排查与方案

1. 定位掉帧原因

使用 GPU 呈现模式分析(开发者选项)和 Systrace:

# Systrace 采集 5 秒
python systrace.py -t 5 -o trace.html gfx view
常见原因特征
onBindViewHolder 耗时Trace 中 RecyclerView 的 bind 段很长
图片加载阻塞UI 线程等待 Bitmap decode
布局过深measure/layout 阶段耗时
频繁创建对象GC 停顿导致掉帧
notifyDataSetChanged()全量刷新触发所有 item 重绘

2. 优化清单

// ✅ 使用 DiffUtil 进行局部更新
class MyDiffCallback(
private val old: List<Item>,
private val new: List<Item>
) : DiffUtil.Callback() {
override fun getOldListSize() = old.size
override fun getNewListSize() = new.size
override fun areItemsTheSame(oldPos: Int, newPos: Int) =
old[oldPos].id == new[newPos].id
override fun areContentsTheSame(oldPos: Int, newPos: Int) =
old[oldPos] == new[newPos]
}

// ✅ 或直接用 ListAdapter(内置异步 DiffUtil)
class MyAdapter : ListAdapter<Item, MyViewHolder>(ItemDiffCallback()) {
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
优化手段说明
DiffUtil / ListAdapter增量更新,避免全量 notify
setHasFixedSize(true)列表尺寸不变时跳过 requestLayout
图片预加载Glide 的 preload() + RecyclerView 滑动监听
ViewHolder 复用RecycledViewPool 多 RecyclerView 共享
预取setItemPrefetchEnabled(true)(默认开启)
减少布局层级ConstraintLayout 替代嵌套 LinearLayout
避免 bind 中搞花活不在 bind 里 inflate、正则、日期格式化
// 滑动时暂停图片加载,停止后恢复
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
Glide.with(context).resumeRequests()
} else {
Glide.with(context).pauseRequests()
}
}
})
过度优化的反面

不要为了优化把代码搞得无法维护。先用 Profiler 确认瓶颈在哪,只优化热点路径


面试答题要点

  1. 先用工具定位(Systrace / GPU 呈现模式)
  2. 根据瓶颈对症下药
  3. DiffUtil 是最常考的知识点,要能说清原理(Myers diff)
  4. 提到 RecycledViewPool 共享 ViewHolder 是加分项

相关链接