本文是对RecyclerView源码走读系列文章的总结,涵盖了RecyclerView的核心知识点和面试中常见的问题。文章结合了作者大量的trace图分析,力求用最直白的方式讲解RecyclerView的工作原理。
一、整体架构概览
RecyclerView是Android中用于展示大量数据列表的控件,它的整体工作流程可以分为以下几个阶段:
1
2
3
4
5
6
7
8
9
10
11
|
Choreographer VSync信号
↓
onMeasure (测量)
↓
dispatchLayoutStep1 (预布局-可选)
↓
dispatchLayoutStep2 (正式布局)
↓
dispatchLayoutStep3 (动画执行)
↓
draw (绘制)
|
关键点:dispatchLayoutStep1(预布局)只有在有动画(增删改)的情况下才会执行。
二、测量布局流程详解
2.1 onMeasure阶段
RecyclerView的onMeasure会根据测量模式来决定是否测量子item:
- 固定宽高模式:直接跳过子item测量
- 非固定模式:调用dispatchLayout进行完整布局
2.2 dispatchLayout三阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
┌─────────────────────────────────────────────────────────────┐
│ dispatchLayoutStep1 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. detachAndScrapAttachedViews │ │
│ │ - 将屏幕可见的ViewHolder分离 │ │
│ │ - update的放入changeScrap,非update放入attachScrap│ │
│ │ 2. fill布局 │ │
│ │ - 填充预布局需要的额外表项 │ │
│ │ 3. 保存预布局位置到ViewInfoStore │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ dispatchLayoutStep2 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. detachAndScrapAttachedViews │ │
│ │ - 再次分离ViewHolder │ │
│ │ 2. fill布局 │ │
│ │ - 根据最终可用空间填充表项 │ │
│ │ - 从scrap缓存中获取ViewHolder │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ dispatchLayoutStep3 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. process动画 │ │
│ │ - 根据预布局和后布局的位置差值执行动画 │ │
│ │ 2. 清空scrap缓存 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
|
三、缓存机制(核心重点)
RecyclerView的缓存机制是面试中最常问的知识点,我们需要从分类、回收、复用三个维度来理解。
3.1 缓存分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
┌─────────────────────────────────────────────────────────────────────┐
│ RecyclerView 缓存层级 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Scrap缓存 │ │ CacheView │ │ RecycledViewPool │ │
│ ├─────────────┤ ├─────────────┤ ├──────────────────────┤ │
│ │ changeScrap │ │ 默认容量:2 │ │ 默认容量:5/类型 │ │
│ │ attachScrap │ │ 无需重新bind │ │ 需重新bind │ │
│ ├─────────────┤ ├─────────────┤ ├──────────────────────┤ │
│ │ 仅layout期间│ │ 滑动时使用 │ │ cache满时转移 │ │
│ │ 使用 │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ViewCacheExtension │ │
│ │ 开发者自定义缓存(很少使用) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
|
各缓存特点:
| 缓存类型 |
容量 |
是否需要bind |
使用场景 |
开发者可控 |
| changeScrap |
一屏数量 |
否 |
预布局中的更新项 |
❌ |
| attachScrap |
一屏数量 |
否 |
预布局中的非更新项 |
❌ |
| CacheView |
2个 |
否 |
滑动离屏缓存 |
✅ |
| RecycledViewPool |
5个/类型 |
是 |
按viewType缓存 |
✅ |
| ViewCacheExtension |
自定义 |
自定义 |
扩展缓存 |
✅ |
3.2 缓存回收时机
非滚动时(layout期间):
- detachAndScrapAttachedViews:将可见ViewHolder分离并放入scrap缓存
- update的放入changeScrap,非update的放入attachScrap
滚动时:
- 滑出屏幕的ViewHolder先放入CacheView
- CacheView满时,移除第一个元素并放入RecycledViewPool
3.3 缓存复用顺序(面试高频)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
获取ViewHolder的完整流程:
1. changeScrap (预布局专用)
↓
2. attachScrap (通过position查找)
↓
3. CacheView (通过position查找)
↓
4. attachScrap (通过id查找,需setHasStableIds)
↓
5. ViewCacheExtension (开发者扩展)
↓
6. RecycledViewPool (通过viewType查找)
↓
7. createViewHolder (创建新的)
↓
8. bindViewHolder (绑定数据)
|
3.4 bindViewHolder触发条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
触发bindViewHolder的条件(满足任一即可):
┌────────────────────────────────────────────────────────────┐
│ 1. 没有bound过 │
│ - createViewHolder创建的 │
│ - 从RecycledViewPool取出的 │
├────────────────────────────────────────────────────────────┤
│ 2. needsUpdate 标志位为true │
│ - 调用notifyItemChange时设置 │
├────────────────────────────────────────────────────────────┤
│ 3. isInvalid 标志位为true │
│ - 调用notifyDataSetChanged时设置 │
│ - 从缓存取出时viewType不匹配 │
└────────────────────────────────────────────────────────────┘
|
四、动画原理
4.1 为什么需要Scrap缓存
Scrap缓存的核心目的是支持动画。RecyclerView通过两次布局来记录动画前后的位置信息:
1
2
3
4
5
6
7
8
9
10
|
dispatchLayoutStep1 (预布局):
- 记录动画前的位置信息到ViewInfoStore
- 生成临时表项来记录位置
dispatchLayoutStep2 (后布局):
- 记录动画后的位置信息
- 根据位置差值计算动画
dispatchLayoutStep3 (动画执行):
- 执行ViewInfoStore中记录的动画
|
4.2 ViewInfoStore动画存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 预布局时记录
addToPreLayout(holder, ItemHolderInfo(left, top, right, bottom))
// 后布局时记录
addToPostLayout(holder, ItemHolderInfo(left, top, right, bottom))
// 处理动画
process(callback) {
if (has PRE && POST) {
// 执行位置变化动画
animatePersistence()
} else if (has APPEAR) {
// 执行添加动画
animateAdd()
} else if (has REMOVE) {
// 执行删除动画
animateRemove()
}
}
|
4.3 动画类型
| 动画类型 |
触发条件 |
动画效果 |
| Change |
notifyItemChange |
透明度从1到0 + 新item透明度从0到1 |
| Add |
notifyItemInserted |
透明度从0到1 |
| Remove |
notifyItemRemoved |
透明度从1到0 |
五、预加载机制
5.1 GapWorker工作原理
RecyclerView利用RenderThread的空闲时间进行预加载:
1
2
3
4
5
6
7
8
9
10
11
12
|
┌──────────────────────────────────────────────────────────────┐
│ 帧渲染流程 │
│ │
│ UI Thread ──────→ RenderThread ──────→ SurfaceFlinger │
│ ↓ ↓ ↓ │
│ 绘制数据 渲染数据 合成图层送屏幕 │
│ ↓ ↓ │
│ 空闲时间 ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←← │
│ │
│ ↑ │
│ 预加载的时机! │
└──────────────────────────────────────────────────────────────┘
|
5.2 预加载时机
- 拖动时:onTouchEvent的ACTION_MOVE
- 惯性滑动时:ViewFlinger的run方法
5.3 deadline控制
预加载有一个最晚完成时间(下一帧的VSync时间),如果超过这个时间,预加载会被放弃:
1
2
3
4
5
6
7
|
// 预加载最晚时间
long nextFrameNs = latestFrameVsyncMs + mFrameIntervalNs;
// 如果create或bind超过deadline,则放弃
if (currentTime + avgCreateTime > deadlineNs) {
return null;
}
|
5.4 预加载与正常滚动的缓存区分
1
2
3
4
5
|
// 预加载的ViewHolder放在CacheView前面
// 正常滚动的ViewHolder放在CacheView后面
// 这样预加载的更容易被复用
CacheView结构: [正常滚动项1, 正常滚动项2, 预加载项1, 预加载项2]
|
六、性能优化
6.1 缓存层面优化
- 使用局部刷新:用
notifyItemChange代替notifyDataSetChanged
- payload形式:只更新部分UI,减少bind耗时
- DiffUtil:自动计算数据差异,实现最小更新
- 设置StableIds:
setHasStableIds(true) + 重写getItemId
- 共享RecycledViewPool:多个RecyclerView共享缓存池
6.2 构建过程优化
- 动态创建View:代替XML解析,减少IO和反射
- 减少布局层级:降低inflate时间
- 提前解析XML:预加载到缓存
6.3 绑定过程优化
- 监听器外部传入:避免每次bind创建新listener
- 纯展示逻辑:bind应该是纯粹的UI绑定,不做计算
6.4 其他优化
- setHasFixedSize(true):固定尺寸减少requestLayout
- 预加载开关:
layout.setItemPrefetchEnabled(false)
- 屏外加载:
calculateExtraLayoutSpace自定义预加载数量
七、面试题目汇总
7.1 基础概念类
Q1: RecyclerView的缓存机制?画出缓存层级图
点击查看答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
RecyclerView的缓存从内到外分为5层:
1. Scrap缓存(layout期间使用)
- changeScrap: 存放被更新的ViewHolder
- attachScrap: 存放非更新的ViewHolder
2. CacheView(默认2个)
- 存放滑出屏幕的ViewHolder
- 无需重新bind,可直接使用
3. ViewCacheExtension(开发者自定义)
- 扩展缓存,开发者可自定义实现
4. RecycledViewPool(默认5个/类型)
- 按viewType缓存
- 需要重新bind
5. createViewHolder
- 创建新的ViewHolder
|
Q2: Scrap缓存和CacheView的区别?
点击查看答案
| 区别 |
Scrap缓存 |
CacheView |
| 使用场景 |
仅在layout期间 |
滚动时使用 |
| 容量 |
一屏可见数量 |
默认2个 |
| 查找方式 |
position或id |
position |
| 开发者可控 |
否 |
是 |
| 是否需要bind |
否 |
否 |
7.2 原理机制类
Q3: 什么情况下会触发onCreateViewHolder和onBindViewHolder?
点击查看答案
触发onCreateViewHolder:
触发onBindViewHolder:
- 没有bound过(isBound=false)
- needsUpdate=true(notifyItemChange)
- isInvalid=true(notifyDataSetChanged)
注意:
- 从Scrap/CacheView取出不需要bind
- 从RecycledViewPool取出需要bind
Q4: RecyclerView如何实现局部刷新?
点击查看答案
- 使用
notifyItemChange代替notifyDataSetChanged
- 更新项会被放入changeScrap,预布局时不会复用
- 后布局时会重新创建和绑定该位置的数据
- 结合payload参数可以实现只更新部分UI
1
2
3
4
5
6
|
// 普通局部刷新
adapter.notifyItemChanged(position)
// payload局部刷新
adapter.notifyItemChanged(position, payload)
// onBindViewHolder(holder, position, payloads)判断payloads
|
Q5: notifyItemChange和notifyDataSetChange的区别?
点击查看答案
| 区别 |
notifyItemChange |
notifyDataSetChange |
| 复用性 |
changeScrap缓存 |
全部缓存失效 |
| create次数 |
少量 |
全部重新创建 |
| bind次数 |
少量 |
全部重新绑定 |
| 动画 |
有 |
无 |
| 性能 |
优 |
差 |
notifyDataSetChanged会让所有ViewHolder标记为invalid,进入RecycledViewPool后需要全部重新bind。
Q6: RecyclerView的动画是如何实现的?
点击查看答案
核心是双布局机制:
-
dispatchLayoutStep1(预布局)
- 记录每个ViewHolder的位置到ViewInfoStore
- 如果有增删,会创建临时表项记录位置
-
dispatchLayoutStep2(后布局)
-
dispatchLayoutStep3(动画执行)
- 根据位置差值计算动画
- Change动画:旧item渐隐 + 新item渐显
- Add动画:渐显
- Remove动画:渐隐
Q7: setHasFixedSize(true)为什么能优化性能?
点击查看答案
当item尺寸固定时,RecyclerView尺寸变化不需要重新测量子item:
- 减少measure调用:直接跳过onMeasure中的子item测量
- 减少requestLayout:更新时不会触发父View的measure/layout
- 动画优化:通过Choreographer直接调度layout,不走View树的完整刷新
1
2
3
|
// 设置后,更新数据只会触发dispatchLayout
// 不会触发从DecorView开始的完整measure/layout流程
recyclerView.setHasFixedSize(true)
|
7.3 高级特性类
Q8: RecyclerView的预加载机制?
点击查看答案
GapWorker:利用RenderThread的空闲时间预加载下一帧可能需要的ViewHolder
- 触发时机:拖动和惯性滑动时
- 预加载内容:计算下一个可见位置需要的ViewHolder
- deadline控制:超过下一帧时间会放弃
- 缓存位置:预加载的ViewHolder放在CacheView后面
1
2
3
4
5
6
7
|
预加载流程:
onTouchEvent(MOVE)
→ GapWorker.postFromTraversal()
→ GapWorker.run()
→ prefetch()
→ buildTaskList() // 收集预加载位置
→ flushTasksWithDeadline() // 创建ViewHolder
|
Q9: DiffUtil和notifyItemChange如何选择?
点击查看答案
| 场景 |
推荐方案 |
原因 |
| 数据源完全替换 |
notifyDataSetChanged |
DiffUtil开销更大 |
| 少量局部变化 |
notifyItemChange + payload |
精准控制 |
| 数据源差异计算 |
DiffUtil |
自动计算最优更新 |
| 频繁更新(如弹幕) |
notifyItemRangeChanged |
批量操作 |
DiffUtil优势:
- 自动计算最小更新集
- 自动处理move、change、remove
- 支持RecyclerViewDataConnection
Q10: ViewHolder复用原理?findViewHolderForAdapterPosition和findViewHolderForLayoutPosition的区别?
点击查看答案
复用原理:
- ViewHolder复用是通过缓存池实现的
- Scap缓存:layout期间复用,不重新bind
- CacheView:滚动时复用,不重新bind
- RecycledViewPool:按类型复用,需要重新bind
两者的区别:
| 方法 |
含义 |
动画时差异 |
| getAdapterPosition() |
数据源中的position |
包含被删除的item |
| getLayoutPosition() |
布局呈现的position |
不包含被删除的item |
例如删除第一个item后:
getAdapterPosition() = 1(第二个item的adapter位置)
getLayoutPosition() = 0(第一个item还没被移除)
八、总结
RecyclerView是Android开发中最核心的控件之一,掌握其工作原理对于写出高性能的列表代码至关重要。建议读者:
- 理解缓存层级:这是RecyclerView性能的核心
- 掌握布局流程:理解三阶段布局是理解动画的基础
- 学会性能优化:从缓存和绑定两个维度入手
- 阅读源码:纸上得来终觉浅,绝知此事要躬行
参考文章