RecyclerView 全解:从入门到源码与性能排查
从基本使用、进阶技巧、核心源码到滑动卡顿排查,系统梳理 RecyclerView 的完整学习路径与面试要点。
面试开场:「RecyclerView 滑动时偶发卡顿,帧率从 60 掉到 30,你会怎么排查?」
这道题表面问性能,实际在考察你对 列表从绑定到渲染的整条链路 是否熟悉——Adapter 怎么写只是起点,真正拉开差距的是:缓存复用机制、LayoutManager 测量布局、DiffUtil 增量刷新,以及 Systrace / Perfetto 能不能把 16.6ms 预算花在哪说清楚。
这篇按 学习路径 → 基本使用 → 进阶使用 → 源码机制 → 性能排查 的顺序展开,可作为 RecyclerView 的系统笔记。
学习路径总览
┌─────────────────────────────────────────────────────────────────────┐
│ Level 1 · 基本使用 │
│ Adapter / ViewHolder / LayoutManager / ItemDecoration / ItemAnimator │
└───────────────────────────────┬─────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Level 2 · 进阶使用 │
│ DiffUtil · ListAdapter · Paging · 多类型 · 嵌套滚动 · 共享 Pool │
└───────────────────────────────┬─────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Level 3 · 源码机制 │
│ 四级缓存 · Scrap/Recycled · prefetch · Layout 三阶段 · 嵌套滚动分发 │
└───────────────────────────────┬─────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Level 4 · 性能与工具 │
│ 主线程耗时 · 布局层级 · Bitmap · GC · Trace / Perfetto / JankStats │
└─────────────────────────────────────────────────────────────────────┘| 阶段 | 目标 | 检验标准 |
|---|---|---|
| 基本使用 | 能独立实现标准列表 | 理解 ViewHolder 复用,会选 Linear / Grid / Staggered LayoutManager |
| 进阶使用 | 应对复杂业务列表 | 多类型、分页、局部刷新、CoordinatorLayout 嵌套滚动不踩坑 |
| 源码机制 | 知道「为什么快」 | 能画出四级缓存流转,解释 prefetch 与 onBind 时机 |
| 性能排查 | 线上卡顿可定位 | 用 Trace 找到超 16.6ms 的 bind / measure / draw 阶段 |
一、基本使用
1.1 核心角色
RecyclerView 把 数据展示 与 布局策略 解耦,四个扩展点各司其职:
| 组件 | 职责 | 常见实现 |
|---|---|---|
| Adapter | 数据 ↔ ViewHolder 绑定 | RecyclerView.Adapter<VH> |
| ViewHolder | 持有 Item 视图引用 | 减少 findViewById |
| LayoutManager | 测量、布局、滚动 | LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager |
| ItemDecoration | 分割线、间距、背景 | 自定义 getItemOffsets / onDraw |
| ItemAnimator | 增删改动画 | 默认 DefaultItemAnimator,局部刷新时可关闭 |
class UserAdapter : RecyclerView.Adapter<UserAdapter.VH>() {
private val items = mutableListOf<User>()
inner class VH(val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
val user = items[position]
holder.binding.name.text = user.name
// 图片加载见进阶章节
}
override fun getItemCount() = items.size
}1.2 LayoutManager 选型
| 场景 | 推荐 | 注意点 |
|---|---|---|
| 单列垂直列表 | LinearLayoutManager(context) | 默认;配合 setStackFromEnd(true) 做聊天列表 |
| 网格 | GridLayoutManager(context, spanCount) | SpanSizeLookup 实现 header 占满行 |
| 瀑布流 | StaggeredGridLayoutManager(span, VERTICAL) | Item 高度不一致;避免在 bind 里改 layout params 触发 relayout |
| 横向画廊 | LinearLayoutManager(context, HORIZONTAL, false) | 配合 SnapHelper(PagerSnapHelper) |
1.3 必须养成的习惯
onCreateViewHolder只做 inflate,不做业务逻辑、不加载图片。onBindViewHolder里避免创建对象(字符串拼接、new监听器、匿名内部类)。- 点击事件在 ViewHolder 构造时注册,通过
bindingAdapterPosition取 position,并判断NO_POSITION。 - 列表为空 / 加载中 / 错误 用
ConcatAdapter或单独 header/footer Adapter,而不是在 Activity 里叠一层 View 挡滚动(参见 Phomemo 首页踩坑)。
二、进阶使用
2.1 DiffUtil 与 ListAdapter
全量 notifyDataSetChanged() 会导致 所有可见 Item 重新 bind,滑动时极易掉帧。DiffUtil 在后台线程计算差异,主线程只做局部刷新:
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(old: User, new: User) = old.id == new.id
override fun areContentsTheSame(old: User, new: User) = old == new
}
class UserListAdapter : ListAdapter<User, UserListAdapter.VH>(UserDiffCallback()) {
// onCreateViewHolder / onBindViewHolder 同上
}
// 使用
adapter.submitList(newList) // 自动 diff + 动画| API | 适用场景 |
|---|---|
DiffUtil.calculateDiff() | 大数据集、自定义 diff 策略 |
ListAdapter + AsyncListDiffer | 日常列表,自动后台 diff |
notifyItemRangeChanged(pos, count, payload) | 只更新头像、点赞数等局部字段 |
Payload 局部刷新示例:
override fun onBindViewHolder(holder: VH, position: Int, payloads: MutableList<Any>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
return
}
// payloads 非空时只更新变化字段,避免重载图片
payloads.forEach { /* 更新 likeCount 等 */ }
}2.2 多 ViewType
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1
}
override fun getItemViewType(position: Int): Int =
if (position == 0) TYPE_HEADER else TYPE_ITEM
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
when (viewType) {
TYPE_HEADER -> HeaderVH(...)
else -> ItemVH(...)
}ViewType 种类应 可控且稳定;动态类型过多会导致 RecycledViewPool 缓存碎片化,复用率下降。
2.3 Paging 3 分页
大数据集不要一次性 submitList 万条数据。Paging 3 配合 PagingDataAdapter:
lifecycleScope.launch {
viewModel.pagingFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}底层仍是 RecyclerView,但数据按页加载、占位符与 LoadState 可统一处理。
2.4 嵌套滚动与 CoordinatorLayout
CoordinatorLayout + AppBarLayout + RecyclerView 是常见折叠头布局。要点:
RecyclerView使用app:layout_behavior="@string/appbar_scrolling_view_behavior"- 快速 fling 后反向拖拽若 AppBar 弹跳,是 Behavior 状态机冲突,不是列表性能问题(详见 Fixed AppBarLayout Behavior)
- 外层
ScrollView嵌套内层RecyclerView需启用嵌套Scrolling 或改为单一 RecyclerView + 多 ViewType
2.5 共享 RecycledViewPool
Tab + ViewPager2 多列表场景,共享 Pool 减少 inflate:
val pool = RecyclerView.RecycledViewPool().apply {
setMaxRecycledViews(VIEW_TYPE, 10)
}
recyclerView1.setRecycledViewPool(pool)
recyclerView2.setRecycledViewPool(pool)2.6 图片加载
override fun onBindViewHolder(holder: VH, position: Int) {
Glide.with(holder.itemView)
.load(user.avatarUrl)
.override(avatarSize, avatarSize) // 指定目标尺寸,避免解码过大 Bitmap
.into(holder.binding.avatar)
}
override fun onViewRecycled(holder: VH) {
Glide.with(holder.itemView).clear(holder.binding.avatar)
super.onViewRecycled(holder)
}三、源码机制
RecyclerView 的性能根基是 View 缓存与复用。理解下面这张流转图,面试和排查都有抓手。
3.1 四级缓存
┌──────────────────┐
滑动 / 刷新 │ mAttachedScrap │ 屏幕内、已 detach 待复用
└────────┬─────────┘
│
┌────────▼─────────┐
│ mCachedViews │ 屏幕外、未回收,保留 position 与数据
│ (默认最多 2 个) │
└────────┬─────────┘
│ 超出容量
┌────────▼─────────┐
│ RecycledViewPool │ 按 viewType 分桶,默认每类 5 个
└────────┬─────────┘
│ 仍无可用
┌────────▼─────────┐
│ onCreateViewHolder │ 重新 inflate
└──────────────────┘| 缓存层 | 特点 | 调优 API |
|---|---|---|
| Scrap | 布局暂存,不清数据 | LayoutManager 内部使用 |
| CachedViews | 保留 ViewHolder 状态 | setItemViewCacheSize(n) 增大可加速回滑 |
| RecycledViewPool | 清空 bind 数据,按 type 复用 | setMaxRecycledViews(type, max)、多 RV 共享 |
| Create | 最昂贵 | 减少 ViewType 种类、简化 item 布局 |
3.2 布局三阶段
LayoutManager 布局时依次执行:
onMeasure— 确定 RecyclerView 尺寸onLayoutChildren— 填充、回收、决定哪些 Item 可见scrollHorizontallyBy/scrollVerticallyBy— 滚动时增量布局
卡顿常出在 onLayoutChildren 触发过多 measure。Item 根布局尽量扁平,ConstraintLayout 单层约束优于多层嵌套 LinearLayout。
3.3 Prefetch 预取
API 21+ 默认开启:在 当前帧空闲时 提前 layout + bind 即将进入屏幕的 Item。
(layoutManager as LinearLayoutManager).apply {
isItemPrefetchEnabled = true
initialPrefetchItemCount = 4 // 首次布局预取数量
}若 onBindViewHolder 过重,prefetch 会把耗时 提前到当前帧,反而可能造成 jank——根因仍是 bind 逻辑太重。
3.4 嵌套滚动分发
RecyclerView 实现 NestedScrollingChild3。滚动事件沿 Child → Parent 向上传递(dispatchNestedPreScroll / dispatchNestedScroll),与 AppBarLayout、BottomSheet 协作。自定义 Behavior 时需正确处理 onNestedPreScroll 消费与 unconsumed 传回。
3.5 关键源码入口(AOSP)
| 关注点 | 类 / 方法 |
|---|---|
| 缓存回收 | RecyclerView.Recycler |
| 布局 | LinearLayoutManager.layoutChunk() |
| 滑动 | GapWorker(prefetch)、OverScroller |
| 适配器通知 | AdapterHelper(处理 notify 与 offset) |
| Diff | DiffUtil、AsyncListDiffer |
四、性能问题分析与排查
回到开头的面试题,按 命中率从高到低 排查:
4.1 主线程耗时
症状: Systrace 里 RV OnBindView 或 Choreographer#doFrame 单帧 > 16.6ms。
常见原因:
onBindViewHolder里解析 JSON、查数据库、同步网络- 复杂字符串 / Spannable 每次 bind 重新构建
- 在 bind 里
requestLayout()或修改 LayoutParams
修法: 数据预处理放到 ViewModel / Repository;bind 只做赋值;耗时格式化用缓存或 payload 局部更新。
4.2 布局层级
症状: measure / layout 占比高,GPU 过度绘制严重。
排查:
- Layout Inspector 看层级深度
- 开发者选项 → GPU 过度绘制
- 避免 Item 根布局
wrap_content嵌套wrap_content
修法: Item 用单层 ConstraintLayout;固定 Item 高度(列表项高度已知时);减少 <merge> 误用导致的额外层级。
4.3 Bitmap 解码
症状: bind 时出现 BitmapFactory.decode、Glide EngineJob 在主线程等待。
排查: StrictMode 检测主线程磁盘 / 网络;Trace 看 decode 耗时。
修法:
- Glide
override(w, h)、format(PREFER_RGB_565)(无透明通道时) - 不在 bind 里
BitmapFactory.decodeResource大图 - 占位图 + 淡入,避免空白闪烁导致「体感卡」
4.4 GC 抖动
症状: 滑动时 Memory Profiler 锯齿状,Trace 出现 Background partial concurrent copying GC。
常见原因:
- 每帧大量
new临时对象(字符串、Pair、lambda) notifyDataSetChanged()导致大规模 rebind- 图片缓存策略不当,频繁分配 ByteArray
修法: DiffUtil 替代全量刷新;对象池 / 复用 Spannable;减少 bind 里日志输出。
4.5 DiffUtil 使用不当
| 问题 | 后果 |
|---|---|
areItemsTheSame 写错 | 错误动画、闪烁、甚至 crash |
主线程 calculateDiff 大数据 | 提交列表本身卡顿 |
| 每次 submit 新 ArrayList 但内容相同 | 无意义 diff 开销 |
建议: 优先 ListAdapter;大数据集用 AsyncListDiffer;getChangePayload 返回非空实现精细更新。
4.6 Compose LazyColumn 对比
若项目混用 Compose,Lazy 列表原理类似(composition reuse + prefetch),但重组(Recomposition)是额外成本:
| 维度 | RecyclerView + View | LazyColumn + Compose |
|---|---|---|
| 复用单元 | ViewHolder | Slot / Node 复用 |
| 局部更新 | DiffUtil + payload | key() + 稳定参数减少重组 |
| 图片 | Glide 成熟 | Coil;需注意重组时重复 launch |
| 典型坑 | bind 过重 | 不稳定 lambda 导致整列表重组 |
Compose 瀑布流 + 大图场景若掉帧,先查 remember / key / derivedStateOf 是否用对,再对比同等条件下 RecyclerView + Glide 基线(参见 Jetpack Compose 实践)。
4.7 工具链:Trace、Perfetto、JankStats
| 工具 | 用途 | 使用要点 |
|---|---|---|
| Systrace / Perfetto | 看每帧主线程切片 | 关注 traversal、RV OnBindView、draw;Android Studio Profiler → System Trace |
| Frame Timeline / GPU Rendering | 定位 jank 帧 | 红色帧展开看是 layout 还是 draw |
| JankStats(Jetpack) | 线上 / 调试 jank 统计 | 监听 JankStats.OnFrameListener,上报 frameDurationUiNanos |
| Macrobenchmark | 滑动 FPS、启动对比 | RecyclerViewBenchmark 自定义滑动场景 |
| StrictMode | 开发期抓主线程 IO | 与列表 bind 叠加使用 |
Perfetto 实操步骤(简化):
- 复现卡顿 → 开始 System Trace 录制
- 滑动列表 5~10 秒 → 停止
- 在
Choreographer#doFrame找 > 16ms 的帧 - 展开看是 bind、measure、draw 还是 GC
- 对症回到 4.1~4.5 修法
Android Studio 内置 Profiler 在低端机或内存紧张时本身也会拖慢调试(参见 Android Profiler 笔记)。Trace 文件导出到 Perfetto UI 分析 往往是更轻量的选择。
五、面试回答模板
「RecyclerView 滑动从 60 掉到 30,我会先 Systrace 抓一帧,看超时发生在 bind、layout 还是 draw。」
- 主线程 —
onBindViewHolder是否有 IO、解码、复杂计算 - 布局 — Item 层级、wrap_content 嵌套、过度绘制
- Bitmap — 解码尺寸、Glide override、是否在主线程等磁盘
- GC — 是否频繁
notifyDataSetChanged、bind 里大量 new - 缓存 —
ItemViewCacheSize、共享 Pool、DiffUtil 是否启用 - DiffUtil / Payload — 能否局部刷新而非全量 rebind
- Compose — 若 LazyColumn,查重组范围与 unstable 参数
- 工具 — Perfetto 定帧、JankStats 线上监控、Macrobenchmark 回归
六、延伸阅读
| 主题 | 链接 |
|---|---|
| 面试中的列表卡顿话术 | Android 面试复盘 |
| Systrace / Profiler 使用 | Android Profiler |
| 主线程消息循环 | Handler 机制全解 |
| CoordinatorLayout 嵌套滚动 | Fixed AppBarLayout Behavior |
| Compose 列表对比 | Jetpack Compose |
一句话总结: RecyclerView 的学习曲线,本质是 「会用 → 会优化 → 懂缓存与布局 → 会用 Trace 证明」。把四级缓存和 16.6ms 帧预算刻进肌肉记忆,列表相关的面试和线上问题就都有据可依。