RecyclerView知识点总结与面试指南

本文是对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 预加载时机

  1. 拖动时:onTouchEvent的ACTION_MOVE
  2. 惯性滑动时: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 缓存层面优化

  1. 使用局部刷新:用notifyItemChange代替notifyDataSetChanged
  2. payload形式:只更新部分UI,减少bind耗时
  3. DiffUtil:自动计算数据差异,实现最小更新
  4. 设置StableIdssetHasStableIds(true) + 重写getItemId
  5. 共享RecycledViewPool:多个RecyclerView共享缓存池

6.2 构建过程优化

  1. 动态创建View:代替XML解析,减少IO和反射
  2. 减少布局层级:降低inflate时间
  3. 提前解析XML:预加载到缓存

6.3 绑定过程优化

  1. 监听器外部传入:避免每次bind创建新listener
  2. 纯展示逻辑:bind应该是纯粹的UI绑定,不做计算

6.4 其他优化

  1. setHasFixedSize(true):固定尺寸减少requestLayout
  2. 预加载开关layout.setItemPrefetchEnabled(false)
  3. 屏外加载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如何实现局部刷新?

点击查看答案
  1. 使用notifyItemChange代替notifyDataSetChanged
  2. 更新项会被放入changeScrap,预布局时不会复用
  3. 后布局时会重新创建和绑定该位置的数据
  4. 结合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的动画是如何实现的?

点击查看答案

核心是双布局机制

  1. dispatchLayoutStep1(预布局)

    • 记录每个ViewHolder的位置到ViewInfoStore
    • 如果有增删,会创建临时表项记录位置
  2. dispatchLayoutStep2(后布局)

    • 执行正常布局
    • 再次记录位置信息
  3. dispatchLayoutStep3(动画执行)

    • 根据位置差值计算动画
    • Change动画:旧item渐隐 + 新item渐显
    • Add动画:渐显
    • Remove动画:渐隐

Q7: setHasFixedSize(true)为什么能优化性能?

点击查看答案

当item尺寸固定时,RecyclerView尺寸变化不需要重新测量子item:

  1. 减少measure调用:直接跳过onMeasure中的子item测量
  2. 减少requestLayout:更新时不会触发父View的measure/layout
  3. 动画优化:通过Choreographer直接调度layout,不走View树的完整刷新
1
2
3
// 设置后,更新数据只会触发dispatchLayout
// 不会触发从DecorView开始的完整measure/layout流程
recyclerView.setHasFixedSize(true)

7.3 高级特性类

Q8: RecyclerView的预加载机制?

点击查看答案

GapWorker:利用RenderThread的空闲时间预加载下一帧可能需要的ViewHolder

  1. 触发时机:拖动和惯性滑动时
  2. 预加载内容:计算下一个可见位置需要的ViewHolder
  3. deadline控制:超过下一帧时间会放弃
  4. 缓存位置:预加载的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开发中最核心的控件之一,掌握其工作原理对于写出高性能的列表代码至关重要。建议读者:

  1. 理解缓存层级:这是RecyclerView性能的核心
  2. 掌握布局流程:理解三阶段布局是理解动画的基础
  3. 学会性能优化:从缓存和绑定两个维度入手
  4. 阅读源码:纸上得来终觉浅,绝知此事要躬行

参考文章

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计