Android RecyclerView 性能优化

2 阅读4分钟

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 有两级缓存:

  1. Scrap/Attached: 屏幕内的。
  2. CachedViews: 刚滑出屏幕的(默认是 2 个)。
  3. 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)的赛跑

  1. 逻辑层面:用 DiffUtil 替代全量刷新。
  2. 代码层面onBind 越轻越好。
  3. 布局层面:减少层级,共用 Pool。
  4. 配置层面setHasFixedSize,调整 Cache。

把这几点做到位,你的列表绝对能从“拖拉机”变成“磁悬浮”。