列表滑动卡顿优化
场景
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 确认瓶颈在哪,只优化热点路径。
面试答题要点
- 先用工具定位(Systrace / GPU 呈现模式)
- 根据瓶颈对症下药
- DiffUtil 是最常考的知识点,要能说清原理(Myers diff)
- 提到
RecycledViewPool共享 ViewHolder 是加分项