RecyclerView 滑动卡顿?教你几招让列表“丝般顺滑”
在 Android 开发中,RecyclerView 是我们最熟悉的“老伙计”,也是最容易出性能问题的组件。你是否遇到过这种情况:手指在屏幕上一划,列表却像幻灯片一样一帧一帧地跳?Logcat 里不停弹出 Skipped 60 frames! The application may be doing too much work on its main thread.?
对于追求用户体验的应用来说(特别是 TV 端或者低端机型),掉帧是绝对不能接受的。今天我们不谈源码分析,只谈实战中能直接提升 FPS 的优化手段。
1. 别在 onBindViewHolder 里“通过苦力”
这是新手最容易犯的错。onBindViewHolder 每秒可能被调用几十次,这里面的代码必须快如闪电。
-
错误做法:
- 在里面设置
OnClickListener(每次 bind 都 new 一个对象,内存抖动)。 - 进行复杂的数学计算或日期格式化(
SimpleDateFormat极其耗时)。 - 加载图片时没有指定大小。
- 在里面设置
-
优化方案:
- 点击事件移到
onCreateViewHolder或者 ViewHolder 的init块中。 - 数据预处理:把日期格式化、字符串拼接等逻辑放到 ViewModel 或数据层处理,传给 Adapter 的应该是直接能显示的 String。
- 点击事件移到
2. 告别 notifyDataSetChanged(),拥抱 DiffUtil
如果你还在无脑调用 notifyDataSetChanged(),请立刻停止。这个方法会强制重绘整个屏幕可见的 Item,哪怕你只是改了一个 TextView 的文字。
- 神器:DiffUtil / ListAdapter
DiffUtil是 Google 官方推出的工具,它能计算出新旧数据集的最小差异。 更推荐直接使用ListAdapter(继承自 RecyclerView.Adapter),它内部封装了 AsyncListDiffer。
// 使用 ListAdapter 后的刷新
adapter.submitList(newList)
// 它会自动计算差异,只刷新变化的 Item,甚至伴随优雅的动画
这不仅能提升性能,还能解决“刷新闪烁”的问题。
3. 布局优化:减少层级,拒绝过度绘制
Item 的布局越复杂,测量(Measure)和布局(Layout)的时间就越长。
- 扁平化:如果你的 Item 布局里有
LinearLayout嵌套LinearLayout,尝试用ConstraintLayout把它拍扁。 - 但是... :对于特别简单的 Item,
ConstraintLayout的构建成本反而比FrameLayout/LinearLayout高。如果只是简单的图文混排,不需要复杂的约束,简单的 ViewGroup 性能更好。 - 移除背景色:检查 Item 根布局是否有不必要的背景色(特别是白色背景),这会导致 GPU 过度绘制。
4. 甚至连 XML 都不用?(高级技巧)
如果是极致性能要求(比如即时通讯的消息列表),解析 XML 布局本身也是一种消耗。 有些大厂(如 Telegram)会直接用 Java/Kotlin 代码手写 View,或者使用 AsyncLayoutInflater 在子线程异步加载布局。 注:普通项目不建议这么卷,维护成本太高。
5. 巧用缓存:setItemViewCacheSize
RecyclerView 有两级缓存:
- Scrap/Attached: 屏幕内的。
- CachedViews: 刚滑出屏幕的(默认是 2 个)。
- RecycledViewPool: 真正的复用池。
如果你发现回滑(往回滚)的时候会卡顿,说明 onBindViewHolder 被重新触发了。你可以适当调大 CacheSize,以空间换时间:
// 将刚滑出屏幕的缓存数量从默认的 2 增加到 5 或 10
recyclerView.setItemViewCacheSize(10)
这样,当你快速回滚时,这 10 个 Item 不需要重新 bind 数据,直接显示。
6. 嵌套 RecyclerView 的大杀器:共享 ViewPool
这是很多复杂页面(比如类似 Play Store 或 Netflix 的页面,垂直列表中嵌套水平列表)卡顿的元凶。
默认情况下,每个嵌套的子 RecyclerView 都有自己的 ViewPool。当用户垂直滑动时,每一行的子 RecyclerView 都在不停地创建和销毁 ViewHolder。
优化方案:让所有子 RecyclerView 共用同一个 RecycledViewPool。
// 在外层 Adapter 中
private val viewPool = RecyclerView.RecycledViewPool()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// ...
holder.innerRecyclerView.setRecycledViewPool(viewPool)
}
这能极大地减少内存波动和创建 View 的开销。
7. 图片加载的“防抖”
如果列表中有大量高清大图,滑动时加载图片会抢占 CPU 资源。 我们可以监听 RecyclerView 的滑动状态:
- 滑动时(SCROLL_STATE_FLING) :暂停图片加载框架(Glide/Coil)的任务。
- 静止时(SCROLL_STATE_IDLE) :恢复加载。
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
Glide.with(context).resumeRequests()
} else {
Glide.with(context).pauseRequests()
}
}
})
8. 简单的固定高度:setHasFixedSize(true)
这可能是最简单的一行代码优化。如果你的列表 Item 高度是固定的(不会因为数据内容变化而忽大忽小),请务必设置:
recyclerView.setHasFixedSize(true)
这告诉 RecyclerView:“我的 Item 大小不会变,你在重新布局的时候不需要重新计算我的宽高。”这能避免大量的重复测量工作。
总结
优化 RecyclerView 其实就是一场与 16ms(60fps)的赛跑。
- 逻辑层面:用
DiffUtil替代全量刷新。 - 代码层面:
onBind越轻越好。 - 布局层面:减少层级,共用 Pool。
- 配置层面:
setHasFixedSize,调整 Cache。
把这几点做到位,你的列表绝对能从“拖拉机”变成“磁悬浮”。