RecyclerView 滑动卡顿分析与优化
一、RV Scroll 16ms 正常吗?
处于临界值,很容易卡帧
屏幕刷新率 每帧预算 RV Scroll 16ms 的情况
─────────────────────────────────────────────────
60Hz 16.6ms ⚠️ 几乎占满,稍有波动就掉帧
90Hz 11.1ms 🔴 已经超时!必掉帧
120Hz 8.3ms 🔴 严重超时!
RV Scroll 只是整帧的一部分,一帧的完整流程:
一帧 16.6ms 预算分配:
┃ input (RV Scroll) ┃ animation ┃ traversal (measure+layout+draw) ┃ RenderThread ┃
┃ 16ms! ┃ ┃ ┃ ┃
↑
光这一步就占满了,后续步骤必然导致超时
所以 16ms 的 RV Scroll 不正常,需要优化。
二、Perfetto 中如何定位问题
1. 展开卡帧的完整 Slice 堆栈
▼ com.your.app (pid: 12345)
▼ main (tid: 12345)
Expected Timeline: ┃████┃████┃████┃████┃
Actual Timeline: ┃████┃████┃████████████┃ ← 🔴 红色=超时
│
Choreographer#doFrame ──────────────────────────────── 32ms 🔴
├── input ──────────────────────────────── 17ms ★ 问题在这
│ └── RV Scroll ─────────────────────── 16ms ★★★
│ └── dispatchNestedPreScroll
│ └── scrollByInternal
│ └── fill ────────────────── 14ms ★★★ 核心耗时
│ └── recycler.getViewForPosition
│ └── createViewHolder ── 8ms
│ │ └── onCreateViewHolder
│ │ └── inflate ── 7ms ★ 布局inflate太慢
│ └── bindViewHolder ──── 5ms
│ └── onBindViewHolder
│ └── Glide.load ── 3ms ★ 图片加载
├── animation ─── 0.5ms
├── traversal ─── 8ms
│ ├── measure ── 3ms
│ ├── layout ─── 3ms
│ └── draw ───── 2ms
└── commit
2. 重点关注的 Slice
| Slice 名称 | 含义 | 正常范围 |
|---|---|---|
RV Scroll | RecyclerView 滚动事件处理 | < 3-5ms |
RV CreateView | onCreateViewHolder 调用 | < 2-3ms |
RV OnBindView | onBindViewHolder 调用 | < 1-2ms |
RV FullInvalidate | 全量刷新 | 不应出现 |
RV Prefetch | 预取机制 | 正常行为 |
inflate | XML 布局解析 | < 2ms |
3. 用 SQL 精确查询
-- 查询所有 RV Scroll 耗时
SELECT
ts / 1e6 as ts_ms,
dur / 1e6 as dur_ms,
name
FROM slice
WHERE name LIKE '%RV Scroll%'
ORDER BY dur DESC
LIMIT 50;
-- 查询 RecyclerView 相关的所有操作
SELECT
name,
COUNT(*) as count,
AVG(dur) / 1e6 as avg_ms,
MAX(dur) / 1e6 as max_ms,
MIN(dur) / 1e6 as min_ms
FROM slice
WHERE name LIKE '%RV%'
GROUP BY name
ORDER BY avg_ms DESC;
-- 查看 RV Scroll 期间的子操作分布
SELECT
s.name,
s.dur / 1e6 as dur_ms,
s.depth
FROM slice s
JOIN slice parent ON s.ts >= parent.ts
AND s.ts + s.dur <= parent.ts + parent.dur
AND s.track_id = parent.track_id
AND s.depth = parent.depth + 1
WHERE parent.name LIKE '%RV Scroll%'
AND parent.dur > 10e6 -- 超过10ms的RV Scroll
ORDER BY parent.ts, s.ts;
三、RV Scroll 耗时长的常见原因
RV Scroll 16ms
│
├── ① onCreateViewHolder 太慢(布局复杂/inflate 慢)
│ └── 表现: "RV CreateView" / "inflate" 耗时长
│
├── ② onBindViewHolder 太慢(主线程做了耗时操作)
│ └── 表现: "RV OnBindView" 中有 Binder/IO/计算
│
├── ③ 缓存命中率低(频繁 create 新 ViewHolder)
│ └── 表现: 大量 "RV CreateView" 而非复用
│
├── ④ ViewType 过多(每种类型都需要独立缓存池)
│ └── 表现: 不同 viewType 的 create 交替出现
│
├── ⑤ 嵌套布局过深(measure/layout 慢)
│ └── 表现: "measure" "layout" 耗时长
│
├── ⑥ notify 方式不当(notifyDataSetChanged 全量刷新)
│ └── 表现: "RV FullInvalidate" 出现
│
└── ⑦ ItemDecoration / ItemAnimator 耗时
└── 表现: draw 阶段出现自定义 decoration slice
四、针对性优化方案
① 优化 onCreateViewHolder(减少 inflate 耗时)
问题:布局 XML 复杂,inflate 耗时
// ❌ 复杂嵌套布局
<LinearLayout>
<RelativeLayout>
<FrameLayout>
<ConstraintLayout>
<ImageView/>
<TextView/>
<TextView/>
<LinearLayout>
<TextView/>
<TextView/>
</LinearLayout>
</ConstraintLayout>
</FrameLayout>
</RelativeLayout>
</LinearLayout>
优化:
// ✅ 方案1: 扁平化布局,用 ConstraintLayout 替代多层嵌套
<ConstraintLayout> <!-- 只有一层 -->
<ImageView />
<TextView />
<TextView />
<TextView />
<TextView />
</ConstraintLayout>
// ✅ 方案2: 使用 ViewStub 延迟加载不常用的部分
<ConstraintLayout>
<ImageView />
<TextView />
<ViewStub
android:id="@+id/stub_extra"
android:layout="@layout/item_extra_info"
android:inflatedId="@+id/extra_info" />
</ConstraintLayout>
// ✅ 方案3: 异步 Inflate(AndroidX)
class MyAdapter : RecyclerView.Adapter<VH>() {
// 预创建 ViewHolder 池
private val viewHolderPool = mutableListOf<VH>()
fun preInflate(parent: ViewGroup, count: Int) {
val inflater = AsyncLayoutInflater(parent.context)
repeat(count) {
inflater.inflate(R.layout.item_layout, parent) { view, _, _ ->
viewHolderPool.add(VH(view))
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
// 优先从预创建池中取
return viewHolderPool.removeFirstOrNull()
?: VH(LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false))
}
}
② 优化 onBindViewHolder(减少绑定耗时)
// ❌ 错误:在 bind 中做耗时操作
override fun onBindViewHolder(holder: VH, position: Int) {
val item = items[position]
// ❌ 主线程格式化日期
holder.date.text = SimpleDateFormat("yyyy-MM-dd").format(item.timestamp)
// ❌ 主线程解析 SpannableString
holder.content.text = Html.fromHtml(item.htmlContent)
// ❌ 同步加载图片
val bitmap = BitmapFactory.decodeFile(item.imagePath)
holder.image.setImageBitmap(bitmap)
// ❌ 每次 bind 都 new OnClickListener
holder.itemView.setOnClickListener { onClick(item) }
}
// ✅ 优化后
override fun onBindViewHolder(holder: VH, position: Int) {
val item = items[position]
// ✅ 提前在数据层格式化好
holder.date.text = item.formattedDate // 预处理
// ✅ 提前在后台线程解析好
holder.content.text = item.parsedSpannable // 预处理
// ✅ 异步加载图片
Glide.with(holder.itemView)
.load(item.imageUrl)
.override(100, 100) // 指定尺寸,避免大图解码
.placeholder(R.drawable.ph) // 占位图
.into(holder.image)
// ✅ 复用 listener,通过 position 区分
holder.itemView.tag = position
holder.itemView.setOnClickListener(sharedClickListener)
}
// 共享 listener
private val sharedClickListener = View.OnClickListener { v ->
val pos = v.tag as Int
onClick(items[pos])
}
③ 提高缓存命中率
// ✅ 增大 RecycledViewPool 的缓存容量
recyclerView.recycledViewPool.setMaxRecycledViews(VIEW_TYPE_NORMAL, 20)
recyclerView.recycledViewPool.setMaxRecycledViews(VIEW_TYPE_HEADER, 5)
// ✅ 增大离屏缓存
recyclerView.setItemViewCacheSize(10) // 默认是 2
// ✅ 多个 RecyclerView 共享缓存池(如 ViewPager 中的多个列表)
val sharedPool = RecyclerView.RecycledViewPool()
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)
④ 减少 ViewType 数量
// ❌ 过多的 ViewType
override fun getItemViewType(position: Int): Int {
return when {
items[position].hasImage && items[position].hasVideo -> TYPE_IMAGE_VIDEO // 1
items[position].hasImage -> TYPE_IMAGE // 2
items[position].hasVideo -> TYPE_VIDEO // 3
items[position].isAd -> TYPE_AD // 4
items[position].isHeader -> TYPE_HEADER // 5
else -> TYPE_NORMAL // 6
}
// 6种 ViewType = 6个独立缓存池 = 缓存命中率低
}
// ✅ 合并 ViewType,用 View.GONE 控制显隐
override fun getItemViewType(position: Int): Int {
return if (items[position].isHeader) TYPE_HEADER else TYPE_NORMAL
// 只有 2 种 ViewType
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.imageView.isVisible = items[position].hasImage
holder.videoView.isVisible = items[position].hasVideo
}
⑤ 使用 DiffUtil 替代 notifyDataSetChanged
// ❌ 全量刷新(触发 RV FullInvalidate)
fun updateData(newItems: List<Item>) {
items = newItems
notifyDataSetChanged() // 所有 ViewHolder 重建!
}
// ✅ 增量刷新
fun updateData(newItems: List<Item>) {
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = items.size
override fun getNewListSize() = newItems.size
override fun areItemsTheSame(old: Int, new: Int): Boolean {
return items[old].id == newItems[new].id
}
override fun areContentsTheSame(old: Int, new: Int): Boolean {
return items[old] == newItems[new]
}
})
items = newItems
diffResult.dispatchUpdatesTo(this)
}
// ✅✅ 最佳:使用 ListAdapter(自动异步 Diff)
class MyAdapter : ListAdapter<Item, VH>(ItemDiffCallback()) {
// submitList(newList) 自动在后台线程计算 diff
}
⑥ 开启预取(Prefetch)
// RecyclerView 默认开启 GapWorker 预取
// 确保没有被关闭
(recyclerView.layoutManager as LinearLayoutManager)
.isItemPrefetchEnabled = true // 默认 true
// 自定义预取数量(嵌套 RecyclerView 场景)
innerLayoutManager.initialPrefetchItemCount = 4
⑦ 其他优化
// ✅ 固定尺寸(避免每次 measure 整个 RecyclerView)
recyclerView.setHasFixedSize(true)
// ✅ 滑动时暂停图片加载
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
Glide.with(rv.context).pauseRequests()
}
RecyclerView.SCROLL_STATE_IDLE -> {
Glide.with(rv.context).resumeRequests()
}
}
}
})
// ✅ 关闭默认动画(如果不需要)
recyclerView.itemAnimator = null
// ✅ 使用 RecyclerView.setItemViewCacheSize 增加缓存
recyclerView.setItemViewCacheSize(20)
五、优化前后 Perfetto 对比
优化前 🔴
Choreographer#doFrame ──────────────────────────────── 35ms
├── input ──────────────────────────────── 18ms
│ └── RV Scroll ─────────────────────── 16ms
│ └── fill ──────────────────────── 14ms
│ ├── createViewHolder ────────── 8ms (inflate 复杂布局)
│ └── bindViewHolder ──────────── 5ms (主线程解析HTML)
├── traversal ──────────────────────────── 12ms
│ ├── measure ────────────────────────── 6ms (深层嵌套)
│ ├── layout ─────────────────────────── 4ms
│ └── draw ───────────────────────────── 2ms
优化后 🟢
Choreographer#doFrame ──────────────────── 8ms
├── input ──────────── 3ms
│ └── RV Scroll ──── 2.5ms
│ └── fill ────── 2ms
│ └── bindViewHolder ── 1.5ms (复用 ViewHolder,轻量 bind)
├── traversal ──────── 4ms
│ ├── measure ────── 1.5ms (扁平布局)
│ ├── layout ─────── 1.5ms
│ └── draw ────────── 1ms
六、优化检查清单
□ 布局层级 ≤ 3 层(用 Layout Inspector 检查)
□ onCreateViewHolder 中的 inflate < 3ms
□ onBindViewHolder < 2ms,无 IO/Binder/重计算
□ 使用 DiffUtil / ListAdapter 而非 notifyDataSetChanged
□ setHasFixedSize(true)
□ 合理的 ViewType 数量(≤ 3-4 种)
□ RecycledViewPool 容量充足
□ 图片异步加载 + 指定尺寸
□ 无主线程 GC 压力(避免 bind 中创建大量临时对象)
□ 预取开启(isItemPrefetchEnabled = true)