Android面试冲击附答案(四)————RecyclerView

4 阅读8分钟

Android面试冲击附答案(四)————RecyclerView


一、面试题与答案

1. RecyclerView的缓存机制,每级的作用和容量?
层级名称存储对象命中条件是否重新 bind默认容量核心价值
1Scrap屏内临时 detach 的 ViewHolderposition 一致❌ 不调用无固定上限(屏内可见数)布局/刷新零开销复用
2CachedViews刚滑出、保留绑定的 ViewHolderposition 一致❌ 不调用(数据未清)2,setItemViewCacheSize() 调整,满了后 FIFO 淘汰快速反向滑动秒回
3Extension自定义缓存自定义规则自定义特殊场景定制复用
4RecycledViewPool按 type 分组、已重置的 ViewHolderviewType 一致✅ 必须调用(数据清空了)5/type,setMaxRecycledViews() 调整跨位置/跨 RV 共享、兜底复用
2. mCachedViews 和 RecycledViewPool 的区别是什么?什么时候用哪个?
  • mCachedViews:缓存刚划出屏幕的 2 个 ViewHolder,适用于快速反向滑动场景,零绑定开销,秒回屏幕。如果是高频来回滑动(短视频、商品列表等),可适当调大容量。
  • RecycledViewPool:核心价值是降低跨位置、跨列表的 ViewHolder 创建成本。适用于不同 RV 但 item 类型相同的场景。
3. mAttachedScrap 和 mChangedScrap 分别在什么场景下使用?
  • mAttachedScrap:全局重布局时,屏内 Item 原地复用、零绑定、保流畅。
  • mChangedScrapnotifyItemChanged 时,预布局算动画、重绑数据、支持局部刷新动画。
4. 多个RecyclerView如何共享RecycledViewPool?适用场景是什么?

适用场景:

  1. 同页面的多个 RecyclerView 有共同的 Item
  2. ViewPager 多页面列表,且布局相同(如新闻页面的新闻列表,ViewHolder 被回收后放入 Pool,后续可直接取用)

注意事项:

  1. 必须保证 Item 的 viewType 一致,否则缓存了也没意义,只会浪费内存
  2. setMaxRecycledViews(viewType, maxCount) 时,maxCount 不要设置过大,否则会导致内存占用过高
  3. 避免在 Adapter 中持有 Pool 引用导致内存泄露
5. notifyDataSetChanged 和 DiffUtil 的区别?为什么推荐DiffUtil?
  • notifyDataSetChanged:刷新所有布局,会销毁并重建所有可见 ViewHolder,触发 onCreateViewHolderonBindViewHolder 的全量调用。
  • DiffUtil:只刷新内容变化的 item,且有动画支持,性能更优。
6. 如何优化RecyclerView的滑动卡顿?你实际做过哪些优化?
  1. 核心优化:避免在 onBindViewHolder 里做耗时操作(IO、复杂 JSON 解析、计算)。比如避免在 onBind 里解析消息内容,而是在接收消息时就解析好并存入实体类。
  2. 图片加载优化:开启内存缓存+磁盘缓存、设置图片尺寸裁剪、开启列表滑动暂停加载。
  3. 布局优化:减少层级,使用 ConstraintLayout。
  4. 缓存优化:增大 mCachedView 提升反向滑动性能;RecycledViewPool 共享同类型 item,减少 onCreateViewHolder
  5. 刷新优化:使用 DiffUtilPayload 精细化刷新。
  6. 设置 setHasFixedSize(true),关闭硬件加速等。
7. setHasFixedSize(true) 的作用和原理?什么情况下不能用?
  • 作用:跳过 onMeasure,只触发变化 item 的 onLayoutonDraw(区别于未设置时的全量触发)。
  • 不能用的情况:宽高会变化时,比如 wrap_content、有折叠/收起等场景。
8. 图片加载导致列表抖动怎么解决?
  1. 固定图片尺寸:如果是自适应尺寸,可使用 ConstraintLayout 的固定宽高比,必须设置占位图避免突然撑开。
  2. 动态图片尺寸:提前计算尺寸,提前设置 item 高度(高度提前缓存,避免在 ViewHolder 中计算)。
  3. 图片加载优化:提前裁剪图片到目标尺寸;开启内存缓存和磁盘缓存,第二次滑动直接从缓存读取;滑动时暂停图片加载。
  4. 布局优化:减少不必要的层级。
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会有什么问题?如何解决?

问题:

  1. 滑动冲突:内外层 RV 抢夺 onTouchEvent 事件导致传递混乱。
  2. 内存问题:内层 RV 的 onMeasure 会被多次调用;双重缓存失效,ViewHolder 无法跨层级复用导致 onCreateViewHolder 调用次数暴增。
  3. 过度绘制、滑动流畅性差。

解决方案:

  1. 优先避免嵌套,使用 multitype 替换。
  2. 如果必须嵌套,核心是解决滑动冲突。
  3. 性能优化:共享 RecycledViewPool,减少 onCreateViewHolder 次数。
  4. 优化测量与绘制:设置固定宽高。
12. RecyclerView嵌套在ScrollView/NestedScrollView里为什么会显示不全?怎么解决?

显示不全的原因:

  1. RecyclerView 的高度测量失效(只测量了少量可见 item 的高度)。
  2. 滑动事件冲突,ScrollView 会拦截所有滚动事件。
  3. 即使是支持嵌套滚动的 NestedScrollView,也会因为 RecyclerView 的 nestedScrollingEnabled 默认开启,导致两者滚动优先级冲突,最终 RecyclerView 高度测量不完整。

解决方案:

  1. 优先避免嵌套,通过多类型 item 实现。
  2. 自定义 RecyclerView 重写 onMeasure 强制计算所有 item 高度。
13. NestedScrolling机制是怎么工作的?

NestedScrolling 机制的作用是嵌套组件协同处理滚动事件,流程如下:

  1. 子 View 在处理滚动前,先询问父 View 是否需要「优先处理」部分滚动距离。
  2. 父 View 处理完后,将剩余滚动距离交给子 View 处理。
  3. 子 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。

使用流程:

  1. 定义 payload(如 PAYLOAD_LIKE_COUNT
  2. 调用 notifyItemChanged(position, payload) 传入 payload
  3. 实现带 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 两种类型对比

mAttachedScrapmChangedScrap
触发场景全局重布局(requestLayoutnotifyItemChanged
是否重新 bind❌ 不需要✅ 需要
核心用途屏内 Item 原地复用支持局部刷新动画(预布局)

Prefetch 时序

Frame N 渲染完成
    │
    ▼
Vsync 信号间隙(GapWorker 工作)
    ├── 预测下一帧需要的 ViewHolder 位置
    ├── 从缓存查找 → 找不到则 onCreate + onBind
    └── 存入 mCachedViews

Frame N+1 滑动到该位置
    └── 直接从 mCachedViews 取,零创建零绑定