跳到主要内容
约 12 分钟8 张卡

RecyclerView

RecyclerView 是通过复用条目视图来展示可滚动数据的 Android 组件。它不为每一条数据永久创建一套 View,而是在布局需要时取得合适的 ViewHolder,绑定当前数据,再把暂时不用的视图纳入复用流程。复用减少创建成本,也要求我们始终分清视图对象、数据身份和列表位置。

1. 面试核心​

先记住这几条,再读正文
  • Adapter 绑定数据,ViewHolder 保存条目视图,LayoutManager 安排布局和滚动;复用视图不等于同一 ViewHolder 永远代表同一条数据。卡 · 组件分工与绑定
  • 布局临时复用、近端缓存与按类型分类的回收池各有用途,命中缓存不一定重新绑定,命中池通常仍要绑定当前数据。卡 · 复用与缓存
  • 数据 ID、viewType 和 position 分别表达身份、视图兼容类型与当前位置,点击和异步回调不能永久保存绑定时的 position。卡 · 三种不同标识
  • DiffUtil 区分“同一条数据”和“显示内容相同”,参与差异比较的列表及字段不能在比较期间原地改变。卡 · DiffUtil 与不可变快照
  • 异步请求可能晚于视图复用完成,回填前要验证请求仍属于当前绑定,并覆盖旧显示状态。卡 · 综合题
  • 分页要处理加载状态、去重和查询代次,缓存池解决视图复用,不能代替分页的数据控制。卡 · 综合题
  • 性能优化要从创建、绑定、测量、绘制和请求链路定位,扩大缓存只是用内存换部分工作,不是统一的优化答案。卡 · 综合题
  • setHasFixedSize(true) 说明 Adapter 内容变化不影响 RecyclerView 自身尺寸,不是声明所有条目高度相同。卡 · 定位列表卡顿

2. 组件分工与绑定​

对象负责什么不应承担的工作
Adapter描述条目类型、创建 ViewHolder、把数据绑定到视图在每次绑定中阻塞等待网络或数据库
ViewHolder持有条目 View 与相关视图引用、绑定状态充当独立且永久有效的数据身份
LayoutManager决定需要哪些条目、测量布局和滚动安排维护业务数据来源与请求重试
ItemDecoration装饰及条目间相关偏移代替整套业务条目布局
ItemAnimator协调条目变化动画修复错误的数据身份或通知
Recycler / RecycledViewPool按状态和类型保存、寻找可复用的 ViewHolder缓存网络响应或数据库查询结果

onCreateViewHolder() 处理创建,onBindViewHolder() 把某条数据展示到已有 ViewHolder。后者可能执行多次,不能假定控件属性仍是刚创建时的默认值。

例如上一条数据把图片隐藏了,下一条有图片时只设置图片内容却忘记恢复可见性,就会出现“某些行图片消失”。绑定必须完整表达当前状态:文本、显隐、选中、透明度、可点击性和占位图都应根据当前数据设置,而不是只处理其中一种分支。

onViewRecycled() 是清理可复用条目资源的入口,但条目离开屏幕不等于立刻进入回收池,也不保证每次重新绑定前都先经过这个回调。因此,新绑定本身也要负责替换过期请求和状态。

3. 复用与缓存​

以下分工以 AndroidX RecyclerView 1.3.2 为具体实现参考;理解用途比背固定“几级缓存”和默认容量更重要。

机制主要作用复用时关注什么
attached / changed scrap在布局和变化处理过程中临时保存相关 ViewHolder布局阶段、位置变化和条目更新状态
mCachedViews保留一部分近期离屏但仍能与 Adapter 数据对应的条目若有效且内容未变,可能直接复用已有绑定
ViewCacheExtension应用可提供的额外查找入口属于可选扩展,不是默认一定存在的一层
RecycledViewPool按 viewType 保存可再次使用的 ViewHolder主要复用视图结构,重新对应数据时需要绑定

查找不是每次都无条件走完同一条链。预布局、稳定 ID、条目是否失效、位置映射和预取时间限制都会影响路径。理解基本方向即可:优先复用仍然适配当前需求的条目,没有可用结构时才创建新的 ViewHolder。

viewType 表示哪些 ViewHolder 结构可以相互替换。标题行和商品行若布局、Holder 类型不同,应使用不同类型。多个 RecyclerView 共享一个池时,相同类型值还必须代表兼容的 Holder 和绑定契约;两个独立 Adapter 都把类型写成 0,不代表它们天然兼容。

setItemViewCacheSize() 调整近端缓存,池还可以按类型配置容量。增加缓存可能减少重复创建或绑定,也会保留更多 View、图片和引用。若缓存已经很大但绑定里仍做复杂计算,继续加容量可能只增加内存压力。

预取会提前准备部分可能即将需要的条目,但受时间和布局策略限制,不承诺未来滚动完全没有创建或绑定成本。无论预取还是缓存,都不能把阻塞操作放进绑定回调。

4. 身份、位置与差异更新​

4.1 三种不同标识​

标识回答的问题例子
业务 ID这是不是同一条数据文章 ID 为 42
viewType能否复用同类视图结构普通文章行或广告行
position现在排在列表的哪里当前第 3 行

在列表开头插入一行,原来 position 为 3 的文章可能变成 4,但文章 ID 没变。点击回调若一直保存旧 position,就可能打开另一篇文章。

需要当前绑定 Adapter 中的位置时,可在事件发生时读取 bindingAdapterPosition,并处理 RecyclerView.NO_POSITION。组合 Adapter 还要区分相对绑定 Adapter 的位置与整个 RecyclerView 中的位置,不能混用索引空间。

稳定 ID 是 Adapter 向 RecyclerView 提供的稳定身份承诺,启用后要保证同一条数据在位置变化时仍使用相同 ID,且当前条目之间不会冲突。它与 DiffUtil 的身份判断相关,但不是同一个开关:使用 ListAdapter 不要求一定开启稳定 ID,开启稳定 ID 也不能修复错误的 Diff 回调。

4.2 DiffUtil 与不可变快照​

DiffUtil 比较旧列表与新列表,计算插入、删除、移动和内容更新等变化。areItemsTheSame() 判断是否同一条业务数据,areContentsTheSame() 判断会影响显示的内容是否相同。

例如 ID 42 的标题由“旧标题”变成“新标题”,应是同一条数据的内容变化;若直接把标题当身份,就可能被当成删除旧条目并插入新条目。

比较期间需要保留旧状态。若先把旧列表里的可变对象改掉,再复制一份只有外层结构不同的列表,旧、新两边可能仍指向同一个已修改对象,DiffUtil 就看不到原来的值。只调用 toList() 不一定形成内容快照;参与比较的对象及字段也要保持稳定。

ListAdapter 利用 AsyncListDiffer 组织差异更新。已有旧列表时,差异计算通常在后台进行,更新通知应用到主线程;第一次提交、清空等简单情况可以直接处理,不应断言每次提交都必然启动一轮后台比较。

在 1.3.2 实现中,同一个列表对象再次提交会走快速返回路径,因此不能靠“改原列表后再次 submit”强迫重算。后台差异结果还会检查提交代次,防止旧计算覆盖后来的列表。某次提交被更新提交取代时,对应 commit callback 不保证执行,不能把它当作业务请求必达的完成通知。

4.3 最小 ListAdapter 示例​

下面用不可变字段表示显示快照,点击时读取当前有效位置。示例只用标准 TextView,便于把关注点放在身份和绑定上。

import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

data class NewsRow(val id: Long, val title: String, val selected: Boolean)

object NewsDiff : DiffUtil.ItemCallback<NewsRow>() {
override fun areItemsTheSame(oldItem: NewsRow, newItem: NewsRow) =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: NewsRow, newItem: NewsRow) =
oldItem == newItem
}

class NewsAdapter(private val onOpen: (Long) -> Unit) :
ListAdapter<NewsRow, NewsAdapter.Holder>(NewsDiff) {

class Holder(val titleView: TextView) : RecyclerView.ViewHolder(titleView)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val text = TextView(parent.context).apply {
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
val holder = Holder(text)
text.setOnClickListener {
val position = holder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onOpen(getItem(position).id)
}
}
return holder
}

override fun onBindViewHolder(holder: Holder, position: Int) {
val row = getItem(position)
holder.titleView.text = row.title
holder.titleView.isSelected = row.selected
}
}

fun updateTitle(adapter: NewsAdapter, id: Long, title: String) {
val next = adapter.currentList.map { row ->
if (row.id == id) row.copy(title = title) else row
}
adapter.submitList(next)
}

NewsDiff 用 ID 判断身份,用三个字段的值判断内容;标题改变会触发内容更新,不会让文章变成另一条数据。updateTitle() 创建新的列表和变化行,未变化的不可变行可以继续共享。

这里的 selected 只是演示完整设置状态,具体选中样式由 TextView 的背景等配置决定。updateTitle() 也只是一次基于当前显示列表的更新示例:连续快速操作不能都从尚未提交的 currentList 推导业务状态,否则可能互相覆盖。正式应用应在状态持有者中维护最新模型,再向 Adapter 提交快照。

4.4 payload 与完整绑定​

payload 是内容局部变化时可以传给绑定过程的补充信息,例如只有收藏状态改变,可减少无关更新。但 payload 可能因条目未附着等情况被丢弃,也可能合并多次变化;空 payload 必须支持完整绑定。

手动调用 notifyItemInserted()、notifyItemRemoved() 等方法时,底层数据变化与通知的索引必须一致。通知不是修改数据本身。使用 ListAdapter 提交差异时,也不要在外部再随意修改它使用的列表并混发另一套通知。

5. 异步结果与复用​

异步绑定错误是请求开始时 ViewHolder 代表 A,结果返回时已经绑定 B,却仍把 A 的内容写进去。网络请求取消能减少浪费,但取消不一定阻止已经完成或排队的回调,因此还需要身份检查。

一个可靠的处理顺序是:新绑定先取消或替换旧请求,设置占位状态,记录当前条目与本次请求标识;回调到主线程后验证它仍匹配,再更新显示。只比较业务 ID 有时也不够:同一条数据的图片 URL 变化,或者同一 ID 发起了新的请求,还需要版本、URL 或请求代次来区分。

时刻ViewHolder 当前绑定请求状态
开始加载A,请求代次 1A 图片正在加载
视图复用B,请求代次 2取消 A 或将其标记过期,显示 B 占位图
A 结果晚到仍为 B、代次 2因身份或代次不匹配而丢弃
B 结果到达仍为 B、代次 2更新 B 图片

“当前 View 仍然附着在窗口上”不能证明结果属于它;它可能一直附着,只是绑定数据变了。图片加载库通常提供与目标 View 关联的请求管理,应按其契约使用,同时处理空 URL、失败和占位状态。

复用也不等于数据缓存。ViewHolder 池、图片解码缓存和网络响应缓存解决的是不同成本,不能把一处命中直接推导成另一处无需工作。

6. 分页与性能​

6.1 分页状态​

分页按批次取得和展示数据,避免一次加载全部记录。列表接近末尾可以成为请求信号,但真正发请求前还要判断是否已经加载中、是否到达末尾、当前查询是否仍有效。

最少需要区分空闲、加载中、失败可重试和已结束,并保存下一页键或游标。一次失败不能直接标成数据末尾,也不能因为滚动回调连续触发就发出许多相同请求。

搜索词从 A 改成 B 时,还要使 A 的分页结果失效。可以通过取消和查询代次校验,避免旧页晚到后被拼接进新结果。刷新与追加同时发生时,也应明确哪一代数据仍能提交;根据稳定 ID 去重,不能只按返回的数组位置判断。

Paging 3 提供分页加载和状态管理等支持,RecyclerView 一侧可通过对应 Adapter 接收分页数据。它仍依赖正确的数据源、页键和错误语义,不会自动修复后端不稳定排序。本文的停止点是能解释请求状态与旧结果处理,不展开完整 Paging 项目配置。

6.2 定位列表卡顿​

先确认卡顿发生在首次展示、快速滚动、图片返回、追加页还是大范围更新,再看具体耗时。

证据指向可评估的处理
大量创建与复杂布局简化条目结构,检查类型分裂,评估合理复用
绑定中有阻塞或重计算把数据准备放到合适的后台流程,提交显示快照
图片尺寸或解码成本过大按显示尺寸加载,使用合适缓存与请求管理
频繁全量通知核对身份和内容,采用正确差异更新
大量测量、布局或绘制结合 Android View 判断实际阶段
内存因缓存持续升高检查缓存容量、图片与持有链,避免无依据扩容

setHasFixedSize(true) 的前提是 Adapter 内容变化不会改变 RecyclerView 自身尺寸,例如列表占据父容器给定的区域。它不要求每行一样高,也不意味着屏幕旋转后 RecyclerView 不能变大。若 RecyclerView 的高度随内容变化,就不能为了性能盲目设置这个承诺。

关闭动画、增加缓存、共享池等都是有条件的选择。应保留同一操作下的耗时、分配和帧证据,说明改善发生在哪一段,也说明换来了多少内存或交互变化。

7 · 面试题卡片​

本页题目与参考回答

7.1 Adapter、ViewHolder 与 LayoutManager 分别负责什么?​

考察重点:数据绑定、视图持有与布局职责。

相关内容:组件分工与绑定。

参考回答

Adapter 描述类型并创建、绑定条目,ViewHolder 保存条目视图及相关状态,LayoutManager 决定布局和滚动时需要哪些条目。 同一 Holder 会绑定不同数据,完整绑定必须覆盖当前显示状态,不能依赖上一次留下的属性,也不能在绑定中阻塞等待数据。

7.2 RecyclerView 的缓存与复用机制是什么?​

考察重点:临时复用、已绑定缓存与按类型回收的差别。

相关内容:复用与缓存。

参考回答

布局阶段有 scrap,近期离屏条目可进入近端缓存,按类型回收的 Holder 可进入 RecycledViewPool,还存在可选扩展入口。 有效近端缓存可能保留绑定;池主要复用结构,重新对应数据通常需要绑定。具体查找受预布局、身份和失效状态影响,缓存容量不能脱离内存成本讨论。

7.3 列表插入数据后,为什么点击可能打开错误条目?​

考察重点:稳定身份、位置变化与回调时机。

相关内容:三种不同标识、最小 ListAdapter 示例。

参考回答

常见原因是回调保存了绑定时的位置,而插入、删除或移动已经改变了它。 事件发生时读取正确 Adapter 空间中的当前位置并处理 NO_POSITION,或者以明确的业务身份执行业务。业务 ID、viewType 与 position 不能相互替代。

7.4 修改对象后再提交列表,为什么 DiffUtil 可能看不出变化?​

考察重点:旧状态保留、身份比较与异步计算。

相关内容:DiffUtil 与不可变快照。

参考回答

旧、新列表可能共享同一个已经被修改的对象,旧值已经丢失;同一个列表对象重新提交还可能走快速返回。 应保留不可变显示快照,用业务 ID 判断同一条数据,用显示相关字段比较内容。复制外层列表不等于复制了可变元素,提交期间也不能继续修改参与比较的字段。

7.5 RecyclerView 快速滚动后出现图片错位,应该怎样分析?​

题型:综合题

考察重点:复用后的异步结果归属与完整绑定。

相关内容:组件分工与绑定、异步结果与复用、三种不同标识。

参考回答

先检查旧请求是否把结果写入已经绑定其他数据的 Holder,以及新绑定是否清空了旧状态。 绑定时设置占位、替换旧请求并记录身份或代次,主线程回调前重新确认。取消是减少工作,身份检查是防止过期写入;同一 ID 的新 URL 也可能需要新请求标识。

7.6 RecyclerView 分页加载如何实现?​

题型:综合题

考察重点:追加、刷新、失败与旧查询结果的关系。

相关内容:分页状态、身份、位置与差异更新、异步结果与复用。

参考回答

至少维护是否加载中、下一页键、是否结束、失败重试以及当前查询代次。 滚动信号要防止重复请求,失败与末尾要区分;查询变化或刷新后拒绝过期追加,并按稳定身份去重。缓存池复用视图,不能替代这些数据状态,Paging 也需要正确的页键与数据源语义。

7.7 怎样定位和优化 RecyclerView 卡顿?​

题型:综合题

考察重点:触发场景、耗时阶段与优化代价。

相关内容:定位列表卡顿、复用与缓存。

参考回答

先复现具体操作,再用帧、trace 和分配证据判断成本在创建、绑定、布局、绘制还是请求回填。 可据此简化布局、移出重计算、改善图片尺寸和正确增量更新。调整缓存或关闭动画必须说明收益与内存、交互代价,不能把扩大缓存当成所有卡顿的答案。

7.8 setHasFixedSize(true) 是否要求所有条目高度相同?​

考察重点:RecyclerView 自身尺寸与子条目尺寸。

相关内容:定位列表卡顿。

参考回答

不要求,它承诺的是 Adapter 内容变化不影响 RecyclerView 自身尺寸。 各条目可以有不同高度,父容器变化仍可改变 RecyclerView 的大小。若列表自身尺寸跟着内容伸缩,就不能无条件开启。

8. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明