0) 先用这一套排查流程(10 分钟复现→定位瓶颈)
-
关动画做“基线”
recyclerView.itemAnimator = null → 先看纯滚动是否顺滑。
-
固定尺寸
recyclerView.setHasFixedSize(true)(数据集大小/Item 尺寸稳定时)。
-
替换 LayoutManager
先用 LinearLayoutManager 验证是否 LM 复杂度导致(线性 > 网格 > 瀑布流 性能)。
-
Diff + payload 代替全量 notifyDataSetChanged(),看“闪烁/掉帧”是否缓解。
-
看图片
关掉图片加载或使用占位(固定比例)验证是否“图太大/解码多”。
-
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 轮迭代内解决。