Android面试冲击附答案(四)————RecyclerView
一、面试题与答案
1. RecyclerView的缓存机制,每级的作用和容量?
| 层级 | 名称 | 存储对象 | 命中条件 | 是否重新 bind | 默认容量 | 核心价值 |
|---|---|---|---|---|---|---|
| 1 | Scrap | 屏内临时 detach 的 ViewHolder | position 一致 | ❌ 不调用 | 无固定上限(屏内可见数) | 布局/刷新零开销复用 |
| 2 | CachedViews | 刚滑出、保留绑定的 ViewHolder | position 一致 | ❌ 不调用(数据未清) | 2,setItemViewCacheSize() 调整,满了后 FIFO 淘汰 | 快速反向滑动秒回 |
| 3 | Extension | 自定义缓存 | 自定义规则 | 自定义 | 无 | 特殊场景定制复用 |
| 4 | RecycledViewPool | 按 type 分组、已重置的 ViewHolder | viewType 一致 | ✅ 必须调用(数据清空了) | 5/type,setMaxRecycledViews() 调整 | 跨位置/跨 RV 共享、兜底复用 |
2. mCachedViews 和 RecycledViewPool 的区别是什么?什么时候用哪个?
- mCachedViews:缓存刚划出屏幕的 2 个 ViewHolder,适用于快速反向滑动场景,零绑定开销,秒回屏幕。如果是高频来回滑动(短视频、商品列表等),可适当调大容量。
- RecycledViewPool:核心价值是降低跨位置、跨列表的 ViewHolder 创建成本。适用于不同 RV 但 item 类型相同的场景。
3. mAttachedScrap 和 mChangedScrap 分别在什么场景下使用?
- mAttachedScrap:全局重布局时,屏内 Item 原地复用、零绑定、保流畅。
- mChangedScrap:
notifyItemChanged时,预布局算动画、重绑数据、支持局部刷新动画。
4. 多个RecyclerView如何共享RecycledViewPool?适用场景是什么?
适用场景:
- 同页面的多个 RecyclerView 有共同的 Item
- ViewPager 多页面列表,且布局相同(如新闻页面的新闻列表,ViewHolder 被回收后放入 Pool,后续可直接取用)
注意事项:
- 必须保证 Item 的
viewType一致,否则缓存了也没意义,只会浪费内存 setMaxRecycledViews(viewType, maxCount)时,maxCount不要设置过大,否则会导致内存占用过高- 避免在 Adapter 中持有 Pool 引用导致内存泄露
5. notifyDataSetChanged 和 DiffUtil 的区别?为什么推荐DiffUtil?
- notifyDataSetChanged:刷新所有布局,会销毁并重建所有可见 ViewHolder,触发
onCreateViewHolder、onBindViewHolder的全量调用。 - DiffUtil:只刷新内容变化的 item,且有动画支持,性能更优。
6. 如何优化RecyclerView的滑动卡顿?你实际做过哪些优化?
- 核心优化:避免在
onBindViewHolder里做耗时操作(IO、复杂 JSON 解析、计算)。比如避免在 onBind 里解析消息内容,而是在接收消息时就解析好并存入实体类。 - 图片加载优化:开启内存缓存+磁盘缓存、设置图片尺寸裁剪、开启列表滑动暂停加载。
- 布局优化:减少层级,使用 ConstraintLayout。
- 缓存优化:增大
mCachedView提升反向滑动性能;RecycledViewPool共享同类型 item,减少onCreateViewHolder。 - 刷新优化:使用
DiffUtil、Payload精细化刷新。 - 设置
setHasFixedSize(true),关闭硬件加速等。
7. setHasFixedSize(true) 的作用和原理?什么情况下不能用?
- 作用:跳过
onMeasure,只触发变化 item 的onLayout和onDraw(区别于未设置时的全量触发)。 - 不能用的情况:宽高会变化时,比如
wrap_content、有折叠/收起等场景。
8. 图片加载导致列表抖动怎么解决?
- 固定图片尺寸:如果是自适应尺寸,可使用 ConstraintLayout 的固定宽高比,必须设置占位图避免突然撑开。
- 动态图片尺寸:提前计算尺寸,提前设置 item 高度(高度提前缓存,避免在 ViewHolder 中计算)。
- 图片加载优化:提前裁剪图片到目标尺寸;开启内存缓存和磁盘缓存,第二次滑动直接从缓存读取;滑动时暂停图片加载。
- 布局优化:减少不必要的层级。
9. LayoutManager的职责是什么?自定义LayoutManager需要实现哪些方法?
核心职责:
- Item 布局管理
- Item 回收与复用调度
- 滚动事件处理
- 测量 RecyclerView 自身尺寸
- 视图填充逻辑
核心方法:
generateDefaultLayoutParams():返回 item 的默认布局参数onLayoutChildren(Recycler recycler, State state):初始化、刷新 item 布局canScrollVertically()/canScrollHorizontally():决定是否垂直/水平滚动
10. RecyclerView.ItemDecoration 的 onDraw 和 onDrawOver 区别?
区别在于是否覆盖 item:
- onDraw:早于 item 绘制,会被 item 内容覆盖。
- onDrawOver:晚于 item 绘制,可以覆盖 item,常用于悬浮头部等效果。
11. RecyclerView嵌套RecyclerView会有什么问题?如何解决?
问题:
- 滑动冲突:内外层 RV 抢夺
onTouchEvent事件导致传递混乱。 - 内存问题:内层 RV 的
onMeasure会被多次调用;双重缓存失效,ViewHolder 无法跨层级复用导致onCreateViewHolder调用次数暴增。 - 过度绘制、滑动流畅性差。
解决方案:
- 优先避免嵌套,使用 multitype 替换。
- 如果必须嵌套,核心是解决滑动冲突。
- 性能优化:共享 RecycledViewPool,减少
onCreateViewHolder次数。 - 优化测量与绘制:设置固定宽高。
12. RecyclerView嵌套在ScrollView/NestedScrollView里为什么会显示不全?怎么解决?
显示不全的原因:
- RecyclerView 的高度测量失效(只测量了少量可见 item 的高度)。
- 滑动事件冲突,ScrollView 会拦截所有滚动事件。
- 即使是支持嵌套滚动的 NestedScrollView,也会因为 RecyclerView 的
nestedScrollingEnabled默认开启,导致两者滚动优先级冲突,最终 RecyclerView 高度测量不完整。
解决方案:
- 优先避免嵌套,通过多类型 item 实现。
- 自定义 RecyclerView 重写
onMeasure强制计算所有 item 高度。
13. NestedScrolling机制是怎么工作的?
NestedScrolling 机制的作用是嵌套组件协同处理滚动事件,流程如下:
- 子 View 在处理滚动前,先询问父 View 是否需要「优先处理」部分滚动距离。
- 父 View 处理完后,将剩余滚动距离交给子 View 处理。
- 子 View 处理完后,再通知父 View 是否需要「收尾处理」。
14. onCreateViewHolder 和 onBindViewHolder 分别在什么时机调用?
- onCreateViewHolder:Inflate Item 布局并创建 ViewHolder,当缓存中没有可用的 ViewHolder 时才会调用。高耗时、低频率。触发时机:没有缓存、缓存失效(
notifyDataSetChanged)。 - onBindViewHolder:绑定数据,更新 UI。高频、低耗时。触发时机:
notifyItemChanged等局部刷新。
15. setHasStableIds(true) 的作用?
告诉 RecyclerView 每个 item 有固定 id,不与 position 绑定(默认绑定 position)。
适用于多 Item 类型、需要 Item 动画、Item 位置频繁变化的列表,保证刷新后 ViewHolder 与数据精准匹配,避免数据错乱。
设置后必须重写 getItemId() 方法,返回唯一 id。
16. RecyclerView的ItemAnimator默认是什么?如何关闭动画?
默认是 DefaultItemAnimator,提供默认的插入、删除等效果。
关闭动画:
rv.itemAnimator = null
17. 局部刷新 notifyItemChanged(position, payload) 中payload的作用?
payload 的核心作用是实现 RecyclerView 的**「精细化局部刷新」**——只更新 Item 中变化的部分 UI。
使用流程:
- 定义 payload(如
PAYLOAD_LIKE_COUNT) - 调用
notifyItemChanged(position, payload)传入 payload - 实现带 payload 的
onBindViewHolder(holder, position, payloads)方法,实现精细化刷新
18. RecyclerView的绘制流程?onMeasure 里做了什么特殊处理?
onMeasure()
└── LayoutManager 接管测量逻辑
onLayout()
└── LayoutManager.onLayoutChildren() // 布局核心方法
回收旧Item(存入缓存)→ 填充新Item → 处理滚动边界
onDraw()
└── 绘制RV自身背景
→ ItemDecoration.onDraw()
→ 绘制Item
→ ItemDecoration.onDrawOver()
onMeasure 特殊处理:将测量逻辑委托给 LayoutManager,支持不同布局策略下的尺寸计算。
19. 预取机制(Prefetch)是怎么工作的?GapWorker是什么?
API 21 默认开启了 RecyclerView 的 Prefetch 机制,核心是利用两次 Vsync 信号之间的空闲时间,提前 create 和 bind 即将划入屏幕的 ViewHolder。
触发时机:滑动时,预取屏幕外 1~2 个位置。必须在下一次 Vsync 信号之前完成,超时则放弃,不影响渲染。
预取流程:
计算位置 → 启动任务(GapWorker)
→ 先从缓存查找(Scrap、CachedView)
→ 没有则 onCreateViewHolder
→ 完成预取后存入 mCachedViews
→ 滑动到该位置后直接从 Cache 取,无需 onCreate 和 bind
GapWorker 是负责执行预取任务的后台工作者,在主线程空闲时调度预取工作。
二、RecyclerView 原理
核心角色
| 角色 | 类名 | 职责 |
|---|---|---|
| 布局管理 | LayoutManager | 决定 Item 如何排列、测量、滚动 |
| 数据适配 | Adapter | 创建 ViewHolder、绑定数据 |
| 视图持有 | ViewHolder | 持有 Item 视图引用,避免重复 findViewById |
| 缓存调度 | Recycler | 管理四级缓存,协调复用逻辑 |
| 动画执行 | ItemAnimator | 处理 Item 增删改的动画效果 |
| 装饰绘制 | ItemDecoration | 绘制分割线、间距等装饰 |
四级缓存结构
┌─────────────────────────────────────────────────────┐
│ RecyclerView │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Scrap │ │ CachedViews │ │ Extension │ │
│ │ (屏内复用)│ │ (刚滑出,2个) │ │ (自定义缓存) │ │
│ │ 零绑定 │ │ 零绑定 │ │ │ │
│ └──────────┘ └──────────────┘ └───────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ RecycledViewPool │ │
│ │ type0: [VH][VH][VH][VH][VH] (max 5) │ │
│ │ type1: [VH][VH] (max 5) │ │
│ │ 需要重新 onBindViewHolder │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
复用查找顺序
需要一个 ViewHolder
│
▼
1. Scrap(mAttachedScrap / mChangedScrap)
│ 未命中
▼
2. CachedViews(position 精确匹配)
│ 未命中
▼
3. Extension(自定义缓存)
│ 未命中
▼
4. RecycledViewPool(viewType 匹配,需重新 bind)
│ 未命中
▼
5. onCreateViewHolder(创建新的)
绘制流程
measure → onMeasure(LayoutManager 接管)
│
layout → onLayout
│ └── LayoutManager.onLayoutChildren()
│ ├── 回收旧 Item → 存入缓存
│ ├── 填充新 Item → 从缓存取或新建
│ └── 处理滚动边界
│
draw → onDraw
│ ├── RV 背景
│ ├── ItemDecoration.onDraw() // 在 item 下方
│ ├── 绘制所有 Item
│ └── ItemDecoration.onDrawOver() // 在 item 上方
Scrap 两种类型对比
| mAttachedScrap | mChangedScrap | |
|---|---|---|
| 触发场景 | 全局重布局(requestLayout) | notifyItemChanged |
| 是否重新 bind | ❌ 不需要 | ✅ 需要 |
| 核心用途 | 屏内 Item 原地复用 | 支持局部刷新动画(预布局) |
Prefetch 时序
Frame N 渲染完成
│
▼
Vsync 信号间隙(GapWorker 工作)
├── 预测下一帧需要的 ViewHolder 位置
├── 从缓存查找 → 找不到则 onCreate + onBind
└── 存入 mCachedViews
Frame N+1 滑动到该位置
└── 直接从 mCachedViews 取,零创建零绑定