RecyclerView 性能优化「系统化排查+落地清单」

84 阅读4分钟

0) 先用这一套排查流程(10 分钟复现→定位瓶颈)

  1. 关动画做“基线”

    recyclerView.itemAnimator = null → 先看纯滚动是否顺滑。

  2. 固定尺寸

    recyclerView.setHasFixedSize(true)(数据集大小/Item 尺寸稳定时)。

  3. 替换 LayoutManager

    先用 LinearLayoutManager 验证是否 LM 复杂度导致(线性 > 网格 > 瀑布流 性能)。

  4. Diff + payload 代替全量 notifyDataSetChanged(),看“闪烁/掉帧”是否缓解。

  5. 看图片

    关掉图片加载或使用占位(固定比例)验证是否“图太大/解码多”。

  6. Perfetto/Systrace 标记:Choreographer 是否长、onBind 是否重、RenderThread 是否爆。


1) 结构 / 布局

✅setHasFixedSize(true)

  • 场景:Item 尺寸与数据集规模基本稳定(常见列表)。

  • 收益:减少 requestLayout() 级联,避免多次测量/布局。

✅ 选对LayoutManager(能简单就别复杂)

  • LinearLayoutManager(最快) > GridLayoutManager > StaggeredGridLayoutManager(最重)。

  • 瀑布流注意:给图片固定宽高比占位;gapStrategy = MOVE_ITEMS_BETWEEN_SPANS(默认)可减“洞”;需要“列稳定”再考虑 NONE 并自行处理空隙。

✅ 避免根布局wrap_content链式测量

  • 能定高就定高(或约束比例 ConstraintLayout dimensionRatio);
  • 减少层级(ConstraintLayout 合并多层 LinearLayout/RelativeLayout)。
  • 不要在 onBind 改 LayoutParams(会触发 re-measure),把这类操作挪到 onCreateViewHolder。

2) 绑定(Bind)与数据更新

✅ 把重活移出onBind

  • 禁止 IO/复杂计算:日期格式化、差值计算、富文本解析 → 后台线程;

  • 文本预计算:PrecomputedText/setTextFuture(大段文本);

  • 监听器只 set 一次:在 onCreateViewHolder 设置 OnClickListener,在 onBind 仅更新数据引用。

✅ 用差分:ListAdapter/AsyncListDiffer+DiffUtil

class ArticleAdapter : ListAdapter<Article, VH>(Diff()) {
  override fun onBindViewHolder(h: VH, pos: Int) = onBindViewHolder(h, pos, emptyList())
  override fun onBindViewHolder(h: VH, pos: Int, payloads: MutableList<Any>) { /* payload 局部绑定 */ }
}
  • 只传新列表实例:submitList(old.toMutableList().apply{…}),不要原地 mutate。

  • payload 增量绑定:在 getChangePayload 返回变化字段;在 onBind(payloads) 里只更新对应控件。

  • 稳定 ID:setHasStableIds(true) + getItemId() → 复用更准、减少闪烁。

✅ 合并更新(滚动中)

  • 把多次 notify* 合并(节流/批量),或者只改数据→submitList 一次,交给差分派发。

3) 图片加载(最大热点)

✅ 预加载与取消

  • Glide:RecyclerViewPreloader(modelProvider, sizeProvider, maxPreload);

  • Coil:ImageLoader.enqueue + ImageRequest 配合 Lifecycle;

  • 在 onViewRecycled(holder) / onDetachedFromWindow 取消未完成请求,避免错位/浪费。

✅ 合理尺寸与占位

  • 按控件尺寸 override(如 Glide .override(width, height))、thumbnail()/sizeMultiplier();
  • 使用固定比例占位(ConstraintLayout dimensionRatio 或自定义 RatioImageView),避免首次测量后再改高度引发抖动。
  • 开启内存缓存/磁盘缓存(默认即可),避免频繁解码。

4) 动画

✅ 高频刷新(计时器/弹幕)→关动画

recyclerView.itemAnimator = null

✅ 其它场景:关“Change 动画”减少闪烁

(recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
  • 真实的 insert/remove/move保留动画,纯内容改变用 payload 做“静默增量”。

5) 复用池 / 预取

✅ 共享RecycledViewPool

val pool = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VT_ARTICLE, 20)
  setMaxRecycledViews(VT_AD, 8)
}
list1.setRecycledViewPool(pool); list2.setRecycledViewPool(pool)
  • 多页面/多 Tab 共享 → 冷启动更少抖动。

  • 多类型需给热点 type 预热 maxRecycledViews,防止“命中不足→频繁 onCreateViewHolder”。

✅ 调整setItemViewCacheSize(n)(适度)

  • 增大“近邻缓存”降低 rebind,但涨内存;一般 4–8 够用。

✅ 预取(尤其嵌套 RV)

  • 内层 LM:setInitialPrefetchItemCount(k)(k ≈ 屏内可见 + 1~2);
  • 外层 RV 的 LinearLayoutManager 需开启默认预取(默认 true)。
  • 自定义 LM:实现 collectAdjacentPrefetchPositions()(高级用法)。

6) 滚动中的数据驱动与合并

  • 滚动时减少 UI 线程工作:批处理数据变更,避免大量小 notify*。
  • 避免在 onBind 里启动昂贵动画(Alpha/Scale/Radius),改为进入屏幕后延迟或只做轻量状态切换。
  • 不要滥用 setIsRecyclable(false) :会击穿复用池,导致内存/卡顿双升;只在短时过渡使用。

7) 监控 / 工具(必要时开火力)

✅ Perfetto / Systrace(首选)

  • 打开 android.view, androidx.recyclerview, Choreographer, RenderThread, Binder。

  • 看:

    • 主线程长片段 → 多为 onBind/Diff 合并/布局测量;

    • RenderThread 长片段 → 大图纹理上传/过度绘制;

    • GapWorker 线程 → 是否出现预取(没有就调 initialPrefetchItemCount/布局方向)。

✅ 其它信号

  • GPU 过度绘制(开发者选项):紫/红 → 背景叠加/ItemDecoration 过多;
  • dumpsys gfxinfo:帧时间分布(查看 P95/P99);
  • 崩溃/ANR/GC:留意 Logcat 中频繁 GC(大图/临时对象多)。

8) 典型组合模板(复制可用)

recyclerView.setHasFixedSize(true)

// LM:线性/网格/瀑布择一
val lm = LinearLayoutManager(context, RecyclerView.VERTICAL, false).apply {
  isItemPrefetchEnabled = true
}
recyclerView.layoutManager = lm

// 共享池 + 近邻缓存
val pool = RecyclerView.RecycledViewPool().apply {
  setMaxRecycledViews(VT_NORMAL, 20)
  setMaxRecycledViews(VT_HEADER, 5)
}
recyclerView.setRecycledViewPool(pool)
recyclerView.setItemViewCacheSize(6)

// Adapter:ListAdapter + 稳定ID + payload
class A : ListAdapter<Row, VH>(RowDiff) {
  init { setHasStableIds(true) }
  override fun getItemId(pos: Int) = getItem(pos).stableId
  override fun onBindViewHolder(h: VH, pos: Int) = onBindViewHolder(h, pos, emptyList())
  override fun onBindViewHolder(h: VH, pos: Int, payloads: MutableList<Any>) { /* 增量绑定 */ }
}

// 图片:按控件尺寸 override + 占位比例;在 onViewRecycled 里 cancel
override fun onViewRecycled(holder: VH) {
  Glide.with(holder.itemView).clear(holder.binding.image)
}

易错点速查

  • 用 notifyDataSetChanged() 当常态 → 换 Diff + payload
  • 在 onBind 里改 LayoutParams/wrap_content 链式测量 → 固定尺寸或比例
  • 图片无占位、原图解码 → override + 占位,必要时降采样。
  • 过度 setIsRecyclable(false) / itemViewCacheSize 巨大 → 复用命中下降/内存爆。
  • getItemViewType 不稳定(用 position 当类型)→ 复用错乱/布局抖动。
  • ConcatAdapter 稳定 ID 冲突 → 设 StableIdMode.ISOLATED_STABLE_IDS 或全局唯一 ID。

一句话收束

先关动画定基线,再用 固定尺寸 + 简单 LM + Diff+payload + 合理图片策略把主因拿下;随后共享池/预取/缓存做细调;最后用 Perfetto 精确定位“谁占了帧”。按这张清单走,大多数“滑不动/掉帧/闪烁”都能在 1–2 轮迭代内解决。