一、基础概念
1.1 什么是 RecyclerView?
RecyclerView 是 Android 提供的用于高效显示大量数据集合的视图组件,可视为 ListView 的升级版,支持列表、网格、瀑布流等多种布局。
主要作用:
| 作用 | 说明 |
|---|---|
| 显示列表数据 | 以列表、网格或瀑布流形式展示数据 |
| 视图复用 | 通过 ViewHolder 复用 item 视图,减少创建与测量 |
| 灵活布局 | 通过 LayoutManager 切换线性、网格、瀑布流等 |
| 动画支持 | 内置 ItemAnimator,支持增删改移动画 |
最简使用示例:
// 布局
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// 代码
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter(dataList)
1.2 RecyclerView 与 ListView 的区别
| 对比项 | RecyclerView | ListView |
|---|---|---|
| 布局管理 | 通过 LayoutManager 支持多种布局 | 仅支持垂直列表 |
| ViewHolder | 强制使用 ViewHolder | 支持但不强制 |
| 动画 | 内置 ItemAnimator | 需自行实现 |
| 分割线 | ItemDecoration 灵活定制 | divider 属性 |
| 缓存 | 多级缓存,复用更充分 | 缓存较简单 |
| 点击事件 | 需在 Adapter 中自己实现 | 内置 setOnItemClickListener |
为何更推荐 RecyclerView: 性能更好(多级缓存)、布局可扩展、官方推荐且持续维护,ListView 已不再更新。
1.3 四大核心组件
RecyclerView 的四个核心角色及关系如下。
组件关系:
graph LR
RV[RecyclerView] --> LM[LayoutManager]
RV --> AD[Adapter]
RV --> IA[ItemAnimator]
AD --> VH[ViewHolder]
(LM 决定排列方式,AD 负责数据到视图,IA 负责动画,VH 缓存 item 引用)
对应代码:
recyclerView.layoutManager = LinearLayoutManager(this) // LayoutManager
recyclerView.adapter = MyAdapter(dataList) // Adapter(内部创建/复用 ViewHolder)
recyclerView.itemAnimator = DefaultItemAnimator() // ItemAnimator(可选)
二、ViewHolder 机制
2.1 什么是 ViewHolder?
ViewHolder 用于缓存 item 根视图及其子 View 的引用,避免在 onBindViewHolder 中重复调用 findViewById(),从而提升滚动性能。
核心思想: 创建或复用时查找一次,后续绑定数据时直接使用引用。
// ❌ 每次绑定都查找,滚动时开销大
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val textView = holder.itemView.findViewById<TextView>(R.id.textView)
textView.text = items[position]
}
// ✅ 在 ViewHolder 中只查找一次,绑定时直接用
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position]
}
2.2 ViewHolder 的优势
| 优势 | 说明 |
|---|---|
| 减少 findViewById 调用 | 例如 1000 个 item、每项 5 个子 View,不缓存则绑定阶段可能产生数千次查找;使用 ViewHolder 后仅在创建时查找一次,复用后直接使用引用。 |
| 减轻 GC | 减少临时对象与重复查找,有利于滑动时的流畅度。 |
| 滑动更流畅 | 绑定阶段耗时下降,帧率更稳定。 |
2.3 自定义 ViewHolder 的写法
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
fun bind(data: MyData) {
textView.text = data.text
}
}
class MyAdapter(private val items: List<MyData>) : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}
三、Adapter
3.1 必须实现的三个方法
| 方法 | 作用 | 调用时机 |
|---|---|---|
onCreateViewHolder() | 创建 ViewHolder | 需要“新”的 ViewHolder 时 |
onBindViewHolder() | 绑定数据到视图 | 每次 item 要显示/更新时 |
getItemCount() | 返回 item 数量 | 布局、滚动、更新时多次调用 |
class SimpleAdapter(private val items: List<String>) :
RecyclerView.Adapter<SimpleAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_simple, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position]
}
override fun getItemCount() = items.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
}
}
3.2 onCreateViewHolder 与 onBindViewHolder 的区别
| 对比项 | onCreateViewHolder | onBindViewHolder |
|---|---|---|
| 调用时机 | 需要新的 ViewHolder 时 | 每次要显示/更新该 item 时 |
| 调用频率 | 相对少(复用为主) | 频繁(滑动、刷新都会触发) |
| 主要职责 | 创建 View + ViewHolder | 把数据填到已有 View 上 |
| 性能关注点 | 布局 inflate、创建开销 | 绑定逻辑要轻量,避免耗时操作 |
建议: 在 onCreateViewHolder 中做一次性初始化;在 onBindViewHolder 中只做数据设置,不做复杂计算或网络请求。
3.3 如何实现点击事件(含长按)
RecyclerView 没有内置 OnItemClickListener,需在 Adapter 内为 itemView 或子 View 设置监听。
| 方式 | 传参形式 | 核心写法 | 优点 | 适用场景 |
|---|---|---|---|---|
| Lambda 回调 | 构造函数传入 (T) -> Unit 等 | holder.itemView.setOnClickListener { ... },回调内用 holder.adapterPosition 取位置并判断 != RecyclerView.NO_POSITION 再访问数据 | 写法简单、调用处直观 | 仅需点击、逻辑简单(推荐) |
| 接口回调 | 构造函数传入 OnItemClickListener | holder.itemView.setOnClickListener { ... },回调中取数据时同样建议用 holder.adapterPosition 并判断 != RecyclerView.NO_POSITION,避免 position 失效 | 便于多实现、可复用接口 | 多处复用同一回调、需要接口抽象时 |
| 点击 + 长按 | 两个 Lambda:(Int, T) -> Unit 与 (Int, T) -> Boolean | 同时设置 setOnClickListener 与 setOnLongClickListener,长按返回 true 表示消费 | 一次封装支持两种操作 | 列表项需长按删除、长按菜单等 |
注意: 在点击/长按回调中若使用 position,建议用 holder.adapterPosition 并判断 != RecyclerView.NO_POSITION,避免回收导致位置失效(见 14.11)。
3.4 多类型视图(多 ViewType)
通过 getItemViewType(position) 区分不同类型,在 onCreateViewHolder 中根据 viewType 创建不同布局的 ViewHolder,在 onBindViewHolder 中按类型绑定。
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) TYPE_HEADER else TYPE_ITEM
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_HEADER -> HeaderViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_header, parent, false))
TYPE_ITEM -> ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_normal, parent, false))
else -> throw IllegalArgumentException("Unknown viewType: $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.bind(headerData) // headerData 需根据实际数据源提供,如 position==0 时的数据或单独的 header 数据
is ItemViewHolder -> holder.bind(items[position - 1])
}
}
3.5 notifyDataSetChanged() 的优缺点
各 notify 方法对比:
| 方法 | 作用 | 是否带动画 | 刷新范围 | 性能 | 优缺点 | 典型场景 |
|---|---|---|---|---|---|---|
notifyDataSetChanged() | 整表刷新 | 否 | 所有可见 item 重新绑定 | 差 | 优:写法简单。缺:无动画、易闪烁、整表 rebind | 简单场景、不关心动画 |
notifyItemInserted(position) | 插入一条 | 是(插入动画) | 仅该 position 及之后布局 | 好 | 优:有动画、局部刷新。缺:需自己维护 position | 列表末尾/中间插入 |
notifyItemRemoved(position) | 删除一条 | 是(删除动画) | 仅该 position 及之后布局 | 好 | 优:有动画、局部刷新。缺:需自己维护 position | 单条删除 |
notifyItemChanged(position) | 更新一条 | 是(变更动画) | 仅该 position | 好 | 优:有动画、只刷一条。缺:整条 rebind | 单条内容变更 |
notifyItemChanged(position, payload) | 带 payload 的更新 | 是,可配合局部刷新 | 仅该 position,可只更新部分 View | 更好 | 优:可只更新部分 View,性能最好。缺:要重写 onBindViewHolder 三参数 | 只改头像/点赞数等局部 |
notifyItemRangeInserted(start, count) | 范围插入 | 是 | 从 start 起共 count 条 | 好 | 优:批量插入有动画。缺:需算准 start、count | 下拉刷新、加载更多 |
notifyItemRangeRemoved(start, count) | 范围删除 | 是 | 从 start 起共 count 条 | 好 | 优:批量删除有动画。缺:需算准 start、count | 批量删除 |
notifyItemRangeChanged(start, count) | 范围更新 | 是 | 从 start 起共 count 条 | 一般 | 优:一次通知多条。缺:范围内都会 rebind | 局部数据变更 |
notifyItemMoved(from, to) | 移动一条 | 是(移动动画) | 仅涉及的两个 position | 好 | 优:移动动画自然。缺:需先改数据再调用 | 拖拽排序、置顶 |
四、LayoutManager
4.1 作用
| 职责 | 说明 |
|---|---|
| 测量子视图 | 参与 RecyclerView 的 onMeasure,测量每个 item 的宽高 |
| 布局子视图 | 在 onLayout 中决定每个 item 的 left/top/right/bottom,即 item 的排列方式 |
| 回收与复用 | 配合 RecyclerView 的 Recycler:回收滑出屏幕的 ViewHolder,在需要时从缓存取用或创建新的 |
4.2 三种常用 LayoutManager
| 类型 | 类名 | 方向/主要参数 | 常用写法 | 典型场景 |
|---|---|---|---|---|
| 线性布局 | LinearLayoutManager | 垂直(默认)/ 水平 / 是否反向 | LinearLayoutManager(context)LinearLayoutManager(context, HORIZONTAL, false)LinearLayoutManager(context, VERTICAL, true) 反向 | 单列列表、横向滑动列表、聊天列表(反向) |
| 网格布局 | GridLayoutManager | 列数(或行数) | GridLayoutManager(context, 3) 3 列可配合 SpanSizeLookup 实现某 item 占多列 | 相册、商品网格、标签墙 |
| 瀑布流 | StaggeredGridLayoutManager | 列数 + 垂直/水平 | StaggeredGridLayoutManager(2, VERTICAL) 2 列垂直StaggeredGridLayoutManager(3, HORIZONTAL) 3 行水平 | 高度不一的卡片流、图片流 |
代码示例:
// 线性:垂直列表(默认)
recyclerView.layoutManager = LinearLayoutManager(this)
// 线性:水平
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
// 网格:例如 3 列
recyclerView.layoutManager = GridLayoutManager(this, 3)
// 瀑布流:2 列垂直
recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
布局形态示意:
LinearLayoutManager (垂直) GridLayoutManager (2 列) StaggeredGridLayoutManager (2 列)
┌─────────┐ ┌─────┬─────┐ ┌─────┬─────┐
│ 1 │ │ 1 │ 2 │ │ 1 │ 2 │
├─────────┤ ├─────┼─────┤ │ ├─────┤
│ 2 │ │ 3 │ 4 │ │ │ 3 │
├─────────┤ └─────┴─────┘ ├─────┤ │
│ 3 │ │ 4 │ │
└─────────┘ └─────┴─────┘
五、缓存机制
RecyclerView 通过多级缓存复用 ViewHolder,避免频繁执行 onCreateViewHolder(inflate 布局)和部分场景下减少 onBindViewHolder(绑定数据),从而提升列表滑动的流畅度与性能。下面按「整体结构 → 各级详解 → 流程示例」说明。
5.1 四级缓存结构
查找顺序(需要显示某个 position 的 item 时):
graph LR
A[要显示] --> B{1.Scrap} --> D{2.Cached} --> E{3.Ext} --> F{4.Pool} --> H[onCreate] --> G[onBind]
B -->|有| C[复用]
D -->|有| C
F -->|有| G
(1→2→3→4 依次查,没有才 onCreate;Scrap/Cached/Pool 见上文表格)
四级对比总表:
| 级别 | 名称 | 存什么 | 容量 | 数据是否清空 | 取出后是否必须 rebind | 典型用途 |
|---|---|---|---|---|---|---|
| 1 | mAttachedScrap | 当前仍属本 RV、未进 Pool 的 ViewHolder(局部刷新时被临时从视图树摘下) | 屏内可见的若干项 | 否 | 是(局部刷新时仅对该项 rebind) | notifyItemChanged 等局部刷新 |
| 2 | mCachedViews | 刚滑出屏幕的 ViewHolder | 默认 2(由系统决定) | 否 | 同 position 复用可不用,否则需 rebind | 快速来回滑动时复用 |
| 3 | ViewCacheExtension | 由开发者自定义 | 自定义 | 自定义 | 自定义 | 特殊缓存策略,一般不用 |
| 4 | RecycledViewPool | 按 viewType 分桶的 ViewHolder | 每 type 默认 5,可调 | 是 | 是 | 跨屏、跨 RV 复用,最后一道复用 |
容量小结(默认): 屏内可见 item 数量随屏幕与 item 高度变化,通常约十来个;mCachedViews 默认 2 个;RecycledViewPool 默认每个 viewType 5 个(可通过 setMaxRecycledViews(viewType, max) 修改)。
要点: 先在一级找 → 没有再二级 → 再没有三级 → 再没有从 Pool 取;Pool 也没有才 onCreateViewHolder 新建。
为什么 mAttachedScrap 要 rebind,而 mCachedViews 有时可以不用?
| 缓存 | 放入时的场景 | 取出时的典型情况 | 是否必须 rebind | 原因简述 |
|---|---|---|---|---|
| mAttachedScrap | 局部刷新(如 notifyItemChanged(position)),即“这条数据已经变了” | 同一轮布局中再次用于同一个 position | 是 | 数据已更新,必须用新数据重新绑定,否则界面仍显示旧内容。 |
| mCachedViews | 单纯滑出屏幕,数据没变,只是暂时不可见 | ① 很快滑回,用于同一个 position ② 用于别的 position(如原 pos 0 的 holder 用来显示 pos 10) | ① 可跳过 ② 必须 | ① 数据未清空且 position 仍匹配,可直接用。 ② 要显示新 position 的数据,必须 rebind。 |
所以:Scrap 是“刷新内容”,必须 rebind;Cached 是“暂存刚滑出的”,同 position 复用时可省一次 rebind。
5.2 一级缓存(mAttachedScrap)
一句话: 一级缓存存的就是当前正在展示的那批 item 对应的 ViewHolder;局部刷新时(如 notifyItemChanged),直接从当前正在展示的这条 item 上复用其 ViewHolder,重新绑定新数据即可,不重新创建、也不从 Pool 取。
- 是什么: RecyclerView 内部的一级缓存,存的是仍属于本 RecyclerView、尚未进入 Pool 的 ViewHolder;局部刷新时这些 item 仍在屏幕内,只是被临时从视图树“摘下来”放入 scrap,便于同 position 复用并 rebind。
- 何时放入: 主要在局部刷新时。例如调用
notifyItemChanged(position),RecyclerView 会先把正在展示该 position 的 ViewHolder 从当前布局中 detach 并放入 mAttachedScrap,再根据新数据重新绑定(rebind)后放回原位置。 - 何时取出: 同一轮布局中,需要填充该 position 时,会优先从 scrap 里取,因为 position 和 viewType 都匹配,无需重新创建、也无需从 Pool 取(Pool 里的数据已被清空)。
- 数据是否清空: 否。Scrap 里的 ViewHolder 仍带着原来的数据,局部刷新时只是对发生变化的项做一次 rebind,其他项可原样复用。
- 效果: 避免因局部刷新就销毁、重建 View,保留焦点、选中态等,动画也更自然。
5.3 二级缓存(mCachedViews)
- 是什么: 存刚滑出屏幕的 ViewHolder,仍在 RecyclerView 的 Recycler 内部,可理解为“屏幕外的短期缓存”。
- 何时放入: 某个 item 随滑动刚离开可视区域时,其 ViewHolder 会先被 detach,然后按策略放入 mCachedViews(若未满)。若 mCachedViews 已满,会把最旧的一个移入 RecycledViewPool。
- 容量: 默认 2,在内存与流畅度之间折中。目的是在“快速来回滑”时,刚滑出的几条还能直接拿来用。
- 数据是否清空: 否。从 mCachedViews 取出的 ViewHolder 仍带着滑出前的数据,若再次显示的 position 恰好对应同一份数据,可减少或避免 rebind。
- 典型场景: 用户向下滑一点再立刻向上滑,刚滑出的 item 很快又进入屏幕,从 mCachedViews 取出复用,减少 onCreate 和 bind 的开销。
5.4 三级缓存(ViewCacheExtension)
- 是什么: 由开发者自定义的一层缓存,是
RecyclerView.ViewCacheExtension的子类,通过RecyclerView.setViewCacheExtension()设置。 - 何时放入 / 取出: 完全由开发者在
getViewForPositionAndType()等回调里自己决定存、取逻辑。 - 典型用途: 极少数场景下需要自定义“按 position 或 viewType 的特殊复用策略”。绝大多数项目不需要实现,用系统默认的一、二、四级即可。
5.5 四级缓存(RecycledViewPool)
- 是什么: 按 viewType 分桶的 ViewHolder 池,ViewHolder 从 mCachedViews 被挤出或直接回收时,会清空数据后放入 Pool。
- 何时放入: ViewHolder 从界面 detach 且不再留在 mAttachedScrap / mCachedViews 时(例如 mCachedViews 满了,或布局阶段决定回收),会调用
recycler.recycleView(holder),内部会把 holder 清空后放入 RecycledViewPool 中对应 viewType 的桶。 - 何时取出: 一、二、三级都没有可用的 ViewHolder 时,按 viewType 从 Pool 里取。取出的 ViewHolder 数据已被清空,必须再走一次
onBindViewHolder才能正确显示。 - 数据是否清空: 是。进入 Pool 前会清空,因此从 Pool 取出的只能当作“空壳”复用,必须 rebind。
- 容量: 每个 viewType 默认最多缓存 5 个(可通过
setMaxRecycledViews(viewType, max)设置)。多个 RecyclerView 可共享同一个 Pool,适合嵌套列表、多 tab 列表等,减少重复创建。
共享 RecycledViewPool 示例:
val sharedPool = RecyclerView.RecycledViewPool()
sharedPool.setMaxRecycledViews(0, 20) // viewType 0 最多缓存 20 个
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)
5.6 流程示例:滑动时 ViewHolder 如何进出一、二级与 Pool
流程图 1:item 滑出屏幕时的去向
graph LR
A[item滑出] --> B{Cached未满?}
B -->|是| C[进Cached]
B -->|否| D[最旧进Pool] --> E[当前进Cached]
流程图 2:需要显示新 position 时 ViewHolder 的来源
graph LR
S{Scrap} -->|有| Show[显示]
S -->|无| C{Cached} -->|无| P{Pool} --> B[onBind] --> Show
C -->|有| Show
P -->|无| N[onCreate] --> B
(Scrap → Cached → Extension → Pool → onCreate,见 5.1)
流程图 3:一次完整滑动的时间线(向下滑再滑回)
sequenceDiagram
participant S as 屏幕
participant C as Cached
participant P as Pool
S->>C: 0,1 滑出存入
S->>C: 2 滑出 Cached 满
C->>P: 0 进 Pool
C->>C: 2 存入
S->>P: 需 10 从 Pool 取
P->>S: onBind(10) 显示
S->>P: 滑回 0 进屏
P->>S: onBind(0) 显示
文字简述(与流程图对应):
- 初始: 屏幕显示 item 0~9,对应 10 个 ViewHolder,都在 RV 上 attach。
- 向下滑: item 0 滑出 → 进 mCachedViews;item 1 滑出 → 进 mCachedViews;item 2 滑出时 mCachedViews 已满(默认 2 个),item 0 的 ViewHolder 移入 RecycledViewPool(清空数据),item 2 的 ViewHolder 进 mCachedViews。
- 需要显示新 item: 要显示 position 10 时,按流程图 2 的顺序查找,最终从 RecycledViewPool 按 viewType 取 ViewHolder(或新建),再 onBindViewHolder(holder, 10) 后 measure、layout 显示。
- 向上滑回: position 0 再次进屏时,从 Pool 取同 type 的 ViewHolder,执行 onBindViewHolder(holder, 0) 后显示,实现“空壳”复用。
整体上:一、二级保留数据、优先复用;四级做跨屏/跨列表的“空壳”复用,必须 rebind。 理解这一点,就能说清 RecyclerView 的缓存机制。
六、性能优化
6.1 常见优化手段汇总
| 序号 | 优化手段 | 说明 |
|---|---|---|
| 1 | ViewHolder + 缓存子 View 引用 | 在 ViewHolder 中缓存 findViewById 结果,避免在 onBindViewHolder 里重复查找,减轻滑动时的开销。 |
| 2 | setHasFixedSize(true) | 列表宽高固定时设置,数据变化时跳过对 RecyclerView 自身的重新测量,见 6.2。 |
| 3 | DiffUtil 增量更新 | 用 DiffUtil 计算差异后 dispatchUpdatesTo,避免整表 notifyDataSetChanged,保留动画、减少 rebind,见 6.3。 |
| 4 | onBindViewHolder 只做轻量操作 | 仅做数据赋值与简单逻辑,不在其中做复杂计算、网络请求或同步 IO,保证每帧耗时可控。 |
| 5 | 布局扁平化 | 减少 item 布局层级,优先用 ConstraintLayout 等减少嵌套,降低 measure/layout 成本。 |
| 6 | 图片异步加载 + 滑动控制 | 用 Glide/Coil 等异步加载图片;滑动时可暂停请求、停止后恢复,并在 onViewRecycled 中取消加载,见 14.7。 |
| 7 | initialPrefetchItemCount 预取 | LinearLayoutManager 等可设置预取数量,提前对即将进入屏幕的 item 做 measure/layout,减轻快速滑动时的卡顿,见 14.8。 |
6.2 setHasFixedSize(true) 的作用
| 项目 | 说明 |
|---|---|
| 含义 | 告诉 RecyclerView“我的尺寸不随 adapter 内容数量变化”,在数据变化时可跳过对自身大小的重新测量,从而略微提升性能。 |
| 适用 | 宽高为 match_parent 或固定 dp。 |
| 不适用 | RecyclerView 自身宽高为 wrap_content 且依赖内容高度时(例如嵌在 ScrollView 内或与 9.2 动态高度 item 同用),不要设 true,见 9.2。 |
当 RecyclerView 的尺寸由自身或父布局约束决定、不随内容变化时,可设置 setHasFixedSize(true)。
6.3 DiffUtil 的使用
DiffUtil 用于计算两个列表的差异,并生成增删改移的更新操作,配合 Adapter.notify 可实现局部刷新和默认动画。
流程概览:
graph LR
A[old+new] --> B[calculateDiff]
B --> C[DiffResult]
C --> D[dispatchUpdatesTo]
D --> E[局部 notify+动画]
示例:
class MyDiffCallback(
private val oldList: List<Item>,
private val newList: List<Item>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].id == newList[newPos].id
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos]
}
}
// 在 Adapter 中
fun updateData(newItems: List<Item>) {
val diffResult = DiffUtil.calculateDiff(MyDiffCallback(items, newItems))
items = newItems.toMutableList()
diffResult.dispatchUpdatesTo(this)
}
注意: ① 数据量大时 DiffUtil.calculateDiff 建议在后台线程执行,再在主线程 dispatchUpdatesTo。② areContentsTheSame 中若用 oldList[oldPos] == newList[newPos],Item 需为 data class 或正确实现 equals(),否则比较的是引用而非内容。
七、ItemDecoration
7.1 作用
| 作用 | 说明 |
|---|---|
| 分割线 | 在 item 之间或四周绘制线条,常用 onDraw() 在 item 下方/右侧画线。 |
| 间距 | 通过 getItemOffsets() 为每个 item 预留 outRect,实现 item 之间的留白或统一边距。 |
| 背景/装饰 | 在 item 区域绘制背景色、圆角、阴影等,可用 onDraw() 或 onDrawOver()。 |
| 预留空间 | getItemOffsets() 为装饰预留绘制区域,避免装饰与 item 内容重叠;也可配合 onDrawOver() 实现悬浮效果。 |
7.2 核心方法(getItemOffsets / onDraw / onDrawOver)
自定义 ItemDecoration 时通常重写以下方法(可选组合):
| 方法 | 调用时机 | 作用 | 说明 |
|---|---|---|---|
| getItemOffsets(outRect, view, parent, state) | 测量、布局 item 之前 | 为当前 item 预留装饰空间 | 通过设置 outRect 的 left/top/right/bottom,给该 item 四周留出空间,后续 item 会避开这部分;不设则装饰可能和 item 重叠或被裁剪。 |
| onDraw(c, parent, state) | 在 item 绘制之前、RecyclerView 的 onDraw 中 | 在 Canvas 上绘制装饰(在 item 之下) | 可遍历 parent.childCount 取每个 child 的边界,在 child 下方/侧边等位置画线或矩形;绘制内容在 item 背后。详见 7.3。 |
| onDrawOver(c, parent, state) | 在 item 绘制之后 | 在 Canvas 上绘制装饰(在 item 之上) | 适合悬浮、遮罩、高亮等盖在 item 上的效果。详见 7.3。 |
7.3 onDraw() 与 onDrawOver() 的区别
| 对比项 | onDraw() | onDrawOver() |
|---|---|---|
| 绘制层级 | 在 item 视图之下绘制 | 在 item 视图之上绘制 |
| 绘制顺序 | 先于 item 绘制(在底层) | 晚于 item 绘制(在最上层) |
| 常见用途 | 分割线、item 间隙背景、底部/侧边装饰 | 悬浮效果、选中遮罩、高亮层、角标、蒙层 |
| 与 item 关系 | 装饰在 item 背后,不遮挡内容 | 可遮挡 item 部分或全部内容,适合叠加层 |
| 典型场景 | 列表分割线、卡片间距底色 | 选中项半透明遮罩、置顶悬浮头、角标/标签 |
绘制顺序(从底到顶):
RecyclerView 背景 → onDraw() 绘制的内容 → item 子视图 → onDrawOver() 绘制的内容。
使用建议: 只做“缝隙里的装饰”(分割线、间距背景)用 onDraw();需要盖在 item 上面的效果(遮罩、悬浮、角标)用 onDrawOver()。
7.4 自定义 ItemDecoration 示例
常见组合: 做分割线时同时用 getItemOffsets() 预留高度 + onDraw() 在预留区域画线,这样分割线不会和 item 内容重叠。
class CustomDividerDecoration(private val height: Int, private val color: Int) :
RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val lastIndex = (parent.adapter?.itemCount ?: 0) - 1
if (lastIndex < 0 || parent.getChildAdapterPosition(view) == lastIndex) return // 最后一项下方不预留
outRect.bottom = height
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val lastIndex = (parent.adapter?.itemCount ?: 0) - 1
if (lastIndex < 0) return
val paint = Paint().apply { this.color = color }
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (parent.getChildAdapterPosition(child) == lastIndex) continue // 最后一项下方不画线
val top = child.bottom.toFloat()
c.drawRect(0f, top, parent.width.toFloat(), top + height, paint)
}
}
}
八、ItemAnimator
8.1 作用与默认实现
| 项目 | 说明 |
|---|---|
| 作用 | 负责 item 的添加、删除、移动、内容变更时的动画效果。 |
| 默认实现 | DefaultItemAnimator(),RecyclerView 默认使用,提供增删改移的淡入淡出与位移动画。 |
| 设置方式 | recyclerView.itemAnimator = DefaultItemAnimator()(也可不设置,系统会使用默认)。 |
8.2 核心方法(自定义时需实现)
| 方法 | 触发时机 | 说明 |
|---|---|---|
| animateAdd(holder) | 调用 notifyItemInserted 等后 | 执行“添加”动画,动画结束需调用 dispatchAddFinished(holder)。 |
| animateRemove(holder) | 调用 notifyItemRemoved 等后 | 执行“删除”动画,动画结束需调用 dispatchRemoveFinished(holder)。 |
| animateMove(holder, fromX, fromY, toX, toY) | 调用 notifyItemMoved 后 | 执行“移动”动画,动画结束需调用 dispatchMoveFinished(holder)。 |
| animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop) | 调用 notifyItemChanged 后 | 执行“内容变更”动画,动画结束需调用 dispatchChangeFinished(oldHolder, newHolder)。 |
| runPendingAnimations() | 内部调度 | 执行当前积压的动画。 |
| endAnimation(holder) / endAnimations() | 需要立即结束动画时 | 结束指定或全部动画。 |
| isRunning() | 查询状态 | 是否还有动画在执行。 |
8.3 使用建议
| 场景 | 建议 |
|---|---|
| 一般列表 | 使用默认 DefaultItemAnimator() 即可,多数项目无需自定义。 |
| 需要禁用动画 | recyclerView.itemAnimator = null 或自定义一个空实现(方法内直接 dispatchFinished)。 |
| 自定义动画 | 继承 RecyclerView.ItemAnimator,实现上述方法,动画结束时务必调用对应的 dispatchXxxFinished(holder),否则 RecyclerView 会认为动画未结束导致异常。 |
九、高级用法
9.1 高级用法概览
| 用法 | 说明 |
|---|---|
| 动态高度 Item | 列表中每条 item 的高度由内容决定、可以不一致(例如有的是一行标题,有的是多行正文)。RecyclerView 支持这种写法,需注意不要误设 setHasFixedSize(true),并尽量用扁平布局以保流畅。 |
| 数据预加载(上拉加载更多) | 用户还没滑到列表最底部时,就提前请求并追加下一页数据,这样滑到底时数据已就绪,减少等待和空白,常用于分页列表、信息流。 |
9.2 动态高度 Item
| 要点 | 说明 |
|---|---|
| 支持情况 | RecyclerView 支持“每条高度不同”:在 item 布局里对高度不固定的 View 使用 wrap_content,RecyclerView 会在布局时测量每条高度,无需额外配置。 |
| setHasFixedSize | setHasFixedSize(true) 表示“RecyclerView 的尺寸不随 adapter 内容数量变化”。若列表在可伸缩容器里或 item 高度变化会导致列表整体高度变化,不要设 true;RecyclerView 宽高由自身或父布局约束决定、不随内容数量变化时(常见为 match_parent 或固定 dp)可设 true,否则可能测量异常或性能变差。 |
| 布局建议 | 动态高度会带来更多 measure 计算。item 布局尽量用 ConstraintLayout 等扁平结构、减少层级,可明显降低单条测量成本,减轻滑动卡顿。 |
9.3 数据预加载(上拉加载更多)
| 要点 | 说明 |
|---|---|
| 实现思路 | 给 RecyclerView 添加 addOnScrollListener,在 onScrolled 里通过 LayoutManager(如 (layoutManager as? LinearLayoutManager)?.findLastVisibleItemPosition())和 adapter.itemCount 判断:当“最后一条可见的 position ≥ itemCount - N”(即距离底部还剩 N 条)时调用加载下一页;N 一般取 3~5。 |
| 防重复 | 用布尔标志位(如 isLoading)或状态标记:发起请求前设为 true,请求结束(成功或失败)后设为 false;在触发加载前先判断,若已在加载中则不再发起新请求,避免快速滑动时重复请求多页。 |
| 请求取消 | 加载下一页多为网络请求。在 Activity/Fragment 销毁或用户离开列表时,应取消未完成的请求(如协程 Job.cancel()、Retrofit Call.cancel()),避免在回调里更新已销毁的 View,造成内存泄漏或崩溃。 |
| 预加载距离 | “距底部还有几条时开始加载”:太小(如 0~1)用户容易滑到底还在等;太大(如 10+)可能浪费流量、增加服务器压力。通常 3~5 条即可,可根据单条高度和屏幕高度微调。 |
十、常见问题
10.1 列表为空的常见原因
| 原因 | 说明与处理 |
|---|---|
| 未设置 LayoutManager | RecyclerView 必须设置 layoutManager 才会排版子 View。处理:recyclerView.layoutManager = LinearLayoutManager(this) 等。 |
| 数据源为空 | Adapter 的 getItemCount() 返回 0 时不会显示任何 item。处理:确认数据列表已赋值、未清空,且 getItemCount() 返回列表 size。 |
| Item 根布局或关键 View 高度为 0 | xml 里 layout_height="0dp" 且约束错误,或权重/比例导致高度为 0。处理:检查 item 布局的宽高和约束,保证可见区域有有效高度。 |
| RecyclerView 高度为 wrap_content 且无约束 | 在 ScrollView、ConstraintLayout 等嵌套下,RecyclerView 可能被测量为 0。处理:改为 match_parent、固定高度,或配合 layout_constraintHeight_min 等给出最小高度。 |
10.2 数据更新导致的异常
| 异常/现象 | 原因与处理 |
|---|---|
| IndexOutOfBoundsException | notifyXXX 的 position 与当前数据源不一致,或先改数据后 notify 的顺序错误。处理:在主线程先更新数据再调用 notify;不要遍历列表时边删边 notify,可先收集要删的 position 再统一删除并 notify。 |
| 并发修改(ConcurrentModificationException 等) | 在遍历列表(如 for (item in list))时直接增删元素。处理:先收集要删除的 position 或新列表,再统一修改并调用对应的 notify。 |
| 位置失效、数据错乱 | 点击等回调里用传入的 position 参数,但 ViewHolder 可能已回收,position 已过期。处理:用 holder.adapterPosition,并判断 != RecyclerView.NO_POSITION 再访问数据(见 14.11)。 |
10.3 ViewHolder 与内存泄漏
| 场景 | 说明与建议 |
|---|---|
| 持有 Activity / 长生命周期对象 | ViewHolder 若持有 Activity 或 Fragment,会阻止其回收。建议:需要 Context 时用 itemView.context.applicationContext;避免在 ViewHolder 或点击监听闭包中直接持有 Activity 引用。 |
| 异步请求未取消 | 在 ViewHolder 内发起网络请求或图片加载,若未在回收时取消,回调可能持有已回收的 View/ViewHolder。建议:在 Adapter 的 onViewRecycled(holder) 里取消该 holder 发起的请求、清除回调或 Glide.clear(holder.imageView) 等。 |
十一、源码与机制简析
本节从“绘制与布局流程”“视图复用机制”“LayoutManager 的职责”三方面简要梳理 RecyclerView 的源码与机制,便于理解其高性能和灵活布局的来源,面试时也能说清原理。
11.1 绘制与布局流程概览
RecyclerView 继承自 ViewGroup,其子 View(即每个 item)的测量、布局、绘制由自身和 LayoutManager 共同完成,整体仍遵循 Android 的 measure → layout → draw 三阶段。
流程示意:
graph LR
M[onMeasure] --> L[onLayout] --> L1[onLayoutChildren] --> L2[回收/取或建/摆放] --> D[onDraw] --> D1[背景-Decoration-子View-onDrawOver]
各阶段说明:
| 阶段 | 入口方法 | 主要行为 |
|---|---|---|
| 测量 | RecyclerView.onMeasure() | 根据自身的 MeasureSpec 和 LayoutManager 的 onMeasure(recycler, state, widthSpec, heightSpec) 决定 RecyclerView 的宽高;子 View(item)的 measure 主要在接下来的 layout 阶段由 LayoutManager 触发。 |
| 布局 | RecyclerView.onLayout() | 内部调用 dispatchLayout(),最终由 LayoutManager.onLayoutChildren(recycler, state) 负责:先回收/ scrap 当前所有子 View,再根据当前滚动偏移和可见区域,按 position 依次取回或新建 ViewHolder、测量、摆放,确定每个 item 的 left/top/right/bottom。 |
| 绘制 | RecyclerView.onDraw(canvas) | 先画自身背景,再依次调用各 ItemDecoration 的 onDraw()(在 item 之下),再绘制每个 item 子 View,最后调用各 ItemDecoration 的 onDrawOver()(在 item 之上)。 |
因此:测量和布局的核心逻辑在 LayoutManager,RecyclerView 主要负责协调 Recycler(缓存)、Adapter(数据与 ViewHolder)、ItemDecoration、ItemAnimator 等;绘制顺序 则是背景 → decoration.onDraw → item → decoration.onDrawOver。
11.2 视图复用流程(简化)
RecyclerView 的高性能很大程度上来自“只创建屏幕内外少量 ViewHolder,通过复用填充大量数据”。复用分为两条线:滑出时回收、需要显示时获取。
流程示意:
flowchart LR
subgraph 滑出
A[滑出] --> B[detach] --> C[Cached或Pool]
end
subgraph 显示
D[要显示] --> E[四级缓存] --> F[onBind] --> G[measure+layout]
end
C -.-> D
(子图间加一条连线可使掘金等平台将两列横向排布)
滑出时的回收:
| 步骤 | 说明 |
|---|---|
| 1. 判定不可见 | LayoutManager 在布局阶段根据当前滚动位置和可视区域,判断哪些 item 已滑出屏幕。 |
| 2. detach / recycle | 滑出的 item 对应 ViewHolder 从 RecyclerView 上 detach,并交给 Recycler:可能先进入 mCachedViews(默认最多 2 个),满则把最旧的移入 RecycledViewPool(按 viewType 分桶,数据会被清空)。 |
| 3. 清空数据 | 进入 Pool 前会清空 ViewHolder 上绑定的数据,以便下次复用给任意同 type 的 position。 |
需要显示时的获取:
| 步骤 | 说明 |
|---|---|
| 1. 按 position 要 View | LayoutManager 在 onLayoutChildren 里对“当前应出现在屏幕上的 position”循环调用 recycler.getViewForPosition(position)(内部会按 viewType 查找)。 |
| 2. 四级缓存查找顺序 | 先查 mAttachedScrap(仍属本 RV、局部刷新时临时摘下的)→ 再查 mCachedViews(刚滑出的、未清数据的)→ 再查 ViewCacheExtension(若有)→ 最后查 RecycledViewPool(按 viewType 取一个);若都没有则 adapter.onCreateViewHolder() 新建。 |
| 3. 绑定与摆放 | 从 Pool 取出或新建的 ViewHolder 会经过 adapter.onBindViewHolder(holder, position) 绑定数据,再经 measure、layout 后显示在 RecyclerView 上。 |
理解这两条线即可说清“为什么滑动流畅”“为什么局部刷新要优先用 scrap”。
11.3 LayoutManager 的职责
LayoutManager 是 RecyclerView 的“布局策略”,负责:测量、布局子 View、回收与复用协调。常见实现有 LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager。
核心方法说明:
| 方法 / 概念 | 职责 |
|---|---|
| onLayoutChildren(recycler, state) | 布局入口。先 detachAndScrapAttachedViews(recycler) 把当前所有子 View 从 RecyclerView 上摘下来并放入 scrap/cache;再根据滚动偏移和可见区域计算“需要显示哪些 position”;对每个需要的 position 调用 recycler.getViewForPosition(position) 取 ViewHolder,measureChildWithMargins 测量、layoutDecorated 摆放。 |
| getViewForPosition(position)(Recycler 提供) | 按四级缓存顺序查找或创建 ViewHolder;若从 Pool 取出或新建,会触发 adapter.onBindViewHolder。 |
| detachAndScrapAttachedViews(recycler) | 把当前 attach 的子 View 全部 detach,并放入 mAttachedScrap 等,供本轮布局中同一 position 或后续复用。 |
| layoutDecorated(child, left, top, right, bottom) | 在考虑 ItemDecoration 的 inset 后,把子 View 布局到指定矩形区域。 |
| fill(recycler, layoutState, state)(以 Linear 为例) | 在某一方向上“填充”可见区域:循环 getViewForPosition、measure、layout,直到可见区域被填满或没有更多数据。 |
与 Recycler、Adapter 的配合:
- Recycler:内部持有 mAttachedScrap、mCachedViews、RecycledViewPool 等,提供
getViewForPosition、recycleView等接口,LayoutManager 只通过 Recycler 取 View、还 View,不直接管理缓存结构。 - Adapter:提供
getItemCount()、getItemViewType(position)、onCreateViewHolder、onBindViewHolder;Recycler 在需要新 ViewHolder 时调 onCreate,在需要绑定数据时调 onBind。 - LayoutManager:只负责“要哪个 position、摆在哪”;“从哪拿 View、还到哪”交给 Recycler,“长什么样、绑什么数据”交给 Adapter。
把这三者分清,就能把“源码与机制”讲清楚:测量与布局以 LayoutManager 为核心,复用以 Recycler 为核心,数据与 View 以 Adapter 为核心。
十二、第三方库与工具
| 库 / 工具 | 类型 | 用途 | 说明 |
|---|---|---|---|
| Epoxy | 第三方库 | 简化 RecyclerView Adapter、多类型列表 | 用注解和 Model 构建列表,自动做 Diff 计算与更新,适合复杂多类型、多状态列表,减少手写 Adapter/ViewHolder 和 notify 逻辑。 |
| Systrace | 系统工具 | 分析列表滑动卡顿、主线程耗时 | 在 onBindViewHolder、onCreateViewHolder 等关键路径加 Trace.beginSection("xxx") / endSection(),用 systrace 录制后查看这些区间的耗时与主线程占用,定位卡顿原因。 |
| Layout Inspector | Android Studio 工具 | 查看运行时 View 层级与属性 | 连接设备后选择进程,可查看当前界面的 View 树、每个 View 的宽高和 margin 等,用于确认 item 布局嵌套深度、测量是否异常。 |
| Android Profiler | Android Studio 工具 | CPU / 内存 / 网络分析 | 可录制 CPU 火焰图、内存分配,配合列表滑动复现卡顿或内存增长,分析是否在 onBind/onCreate 或图片加载上耗时、泄漏。 |
| LeakCanary | 第三方库 | 检测内存泄漏 | 接入后若 Activity/Fragment 等泄漏会弹出通知并给出引用链,便于排查 ViewHolder 或 Adapter 持有导致泄漏。 |
十三、设计模式与职责划分
| 设计模式 / 原则 | 在 RecyclerView 中的体现 | 说明 |
|---|---|---|
| 适配器模式 | Adapter | 将业务数据源适配成 RecyclerView 需要的“ViewHolder + 绑定逻辑”;RecyclerView 只依赖 Adapter 接口(getItemCount、onCreateViewHolder、onBindViewHolder 等),不关心数据具体类型。 |
| 策略模式 | LayoutManager | 布局策略可替换:同一套 Adapter 可搭配 LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager 等,RecyclerView 通过组合不同 LayoutManager 实现不同排列方式。 |
| 对象池模式 | RecycledViewPool | 池中按 viewType 分桶缓存 ViewHolder(每个 ViewHolder 持有 item 的 View)。滑出屏幕时回收进池,需要时从池中取出复用,避免频繁调用 onCreateViewHolder 创建新 View、降低 GC 压力。是典型的对象池:复用“昂贵”对象而非反复创建销毁。 |
| 观察者模式 | Adapter 与 RecyclerView | 数据变化时通过 notifyXXX 通知 RecyclerView,RecyclerView 根据通知类型做局部布局或动画,可视为一种“数据变更 → 视图更新”的观察关系。 |
| 单一职责 | Adapter / ViewModel 分工 | Adapter 只负责“数据 → 视图”的绑定与点击事件转发;业务逻辑、网络请求、页面跳转等建议放在 ViewModel 或 Presenter,通过接口或 Lambda 传入 Adapter,避免 Adapter 臃肿难测。 |
十四、补充要点(面试与实战常考)
本章对前面未单独成章、但面试和实战中常问的要点做集中说明,每条都给出较完整的回答和注意点。
14.1 传统 View 列表与 Compose LazyColumn 的对比
| 对比项 | RecyclerView(传统 View) | Compose LazyColumn |
|---|---|---|
| 技术栈 | 基于 View 系统,需 Adapter、ViewHolder、LayoutManager 配合 | 基于 Jetpack Compose 声明式 UI |
| 写法 | 命令式:先创建 Adapter、再 setAdapter、在 onBind 里手动给 View 赋值 | 声明式:在 LazyColumn 里直接写 item 的 Composable,数据驱动 UI |
| 列表复用 | 通过 ViewHolder 与四级缓存做视图复用 | Compose 内部对 Lazy 列表也有复用与重组机制,思路类似“只渲染可见项” |
| 适用场景 | 现有项目、需兼容低版本、团队熟悉 View 系统 | 新项目、已采用 Compose、希望少写样板代码 |
| 学习成本 | 需理解 Adapter/ViewHolder/缓存等概念 | 需学习 Compose 与状态管理 |
二者在“只渲染可见区域、复用 item”的思路上相近,技术栈和 API 不同;若面试被问到“为什么还用 RecyclerView”,可答兼容性、存量项目、团队技术选型等。
14.2 RecyclerView 相比 ListView 的优势小结
| 优势 | 说明 |
|---|---|
| 布局灵活 | 通过 LayoutManager 可切换线性、网格、瀑布流等,ListView 仅支持垂直列表。 |
| 强制 ViewHolder | 必须用 ViewHolder 持有 item 视图引用,减少 findViewById 与重复创建,ListView 不强制易写漏。 |
| 多级缓存 | 一至四级缓存(Scrap、CachedViews、Extension、Pool),复用更细、滑动更流畅;ListView 缓存较简单。 |
| 内置动画 | ItemAnimator 支持增删改移动画,配合 notify 即可;ListView 需自己实现。 |
| 可扩展 | ItemDecoration、ItemAnimator、自定义 LayoutManager 等都可替换或扩展;ListView 扩展点少。 |
详细对比见 1.2。
14.3 ViewHolder 的生命周期与复用循环
ViewHolder 从创建到被复用、回收,会经历:创建 → 绑定 → 显示 → 回收(或进入缓存)→ 再次绑定(复用),形成循环。
概念流程:
stateDiagram-v2
direction LR
[*] --> 创建
创建 --> 绑定
绑定 --> 显示
显示 --> 回收
回收 --> 绑定
(创建 onCreateViewHolder,绑定 onBindViewHolder,回收即滑出/进缓存)
各阶段注意点:
| 阶段 | 说明与注意 |
|---|---|
| 创建 | 只在“缓存中没有可用的同 type ViewHolder”时调用,应尽量轻量,避免在 onCreateViewHolder 里做耗时或重复逻辑。 |
| 绑定 | 每次显示或刷新该 position 都会调用,只做数据赋值与简单 UI 更新,不做网络请求或重计算。 |
| 回收 | 对应 Adapter 的 onViewRecycled(holder)。应在此取消该 holder 上的图片加载、异步任务、监听器等,避免泄漏和复用时显示错误数据。 |
14.4 notifyItemInserted 与 notifyItemRemoved 的用法与注意
| 方法 | 含义 | 注意点 |
|---|---|---|
| notifyItemInserted(position) | 表示在 position 处新插入了一条数据,RecyclerView 会在该位置插入 item 并播放插入动画。 | 插入后,原 position 及之后的 item 的 position 都会 +1;若只插入一条且后面数据未变,通常不需再 notify 其他项;若一次插入多条,可用 notifyItemRangeInserted(positionStart, itemCount)。 |
| notifyItemRemoved(position) | 表示删除了当前 position 处的数据,RecyclerView 会移除该 item 并播放删除动画。 | 删除后,其后 item 的 position 会 -1;若只删一条,一般不需再通知后续项;批量删除可用 notifyItemRangeRemoved(positionStart, itemCount)。 |
整表结构变化较多时,建议用 DiffUtil 计算差异后统一 dispatch,避免漏 notify 或 position 错乱。
14.5 局部刷新的几种方式
| 方式 | 用法 | 适用场景 |
|---|---|---|
| 单条更新 | notifyItemChanged(position),可选带 payload:notifyItemChanged(position, payload)。若带 payload,需重写 onBindViewHolder(holder, position, payloads),在 payloads 非空时只更新变化部分(如只改头像、点赞数),减少闪烁和开销。 | 某一条数据变更(如点赞数、已读状态)。 |
| 范围更新 | notifyItemRangeInserted / notifyItemRangeRemoved / notifyItemRangeChanged,一次通知一段连续 position 的变化。 | 下拉刷新、批量删除、某一区间数据变更。 |
| 整表差异更新 | 使用 DiffUtil:在后台线程 DiffUtil.calculateDiff(callback),主线程 diffResult.dispatchUpdatesTo(adapter),Adapter 会收到合适的 notify 序列并带动画。 | 整表数据替换、从网络/DB 拉新列表后希望最小化刷新并保留动画。 |
详见 3.5 表格 与 6.3 DiffUtil。
14.6 item 内子 View 的点击事件
RecyclerView 没有类似 ListView 的 onItemClickListener,item 和子 View 的点击都需要在 Adapter 里设置。
| 要点 | 说明 |
|---|---|
| 设置方式 | 在 onBindViewHolder 里对 holder.itemView 或 holder.xxxView 调用 setOnClickListener / setOnLongClickListener,在回调中根据 position(建议用 holder.adapterPosition 并判断 != RecyclerView.NO_POSITION)取数据并处理。 |
| 事件传递 | 若子 View 消费了点击(返回 true),则 item 的点击不会触发;若需要“点 item 跳详情、点按钮做操作”,可分别给 itemView 和 button 设置监听,或在回调里根据 view.id 区分。 |
| 避免错位 | 不要用 onBindViewHolder 的 position 参数闭包进回调,应用 holder.adapterPosition 并在使用前判断 != RecyclerView.NO_POSITION,见 14.11。 |
14.7 列表中的图片加载与滑动卡顿
列表里大量图片若在主线程解码或滑动时仍全量加载,容易造成卡顿。
| 要点 | 说明 |
|---|---|
| 使用异步库 | 使用 Glide、Coil 等库在后台线程解码,并做好内存与磁盘缓存,避免主线程解码和重复请求。 |
| 滑动时暂停 | 在 addOnScrollListener 的 onScrollStateChanged 中,若 newState != RecyclerView.SCROLL_STATE_IDLE,调用 Glide.with(recyclerView.context).pauseRequests();恢复 IDLE 时 resumeRequests(),减少滑动过程中的解码与网络占用,提升流畅度。 |
| 回收时清理 | 在 Adapter 的 onViewRecycled(holder) 里调用 Glide.with(holder.itemView.context).clear(holder.imageView)(或 Coil 的取消方法),避免该 View 复用到其他 position 时仍显示旧图或请求未取消导致泄漏。 |
14.8 initialPrefetchItemCount:ViewHolder 预取
| 项目 | 说明 |
|---|---|
| 含义 | LinearLayoutManager 等提供的属性(非所有 LayoutManager 都有),表示在“即将进入屏幕”的方向上提前多取几个 position 的 ViewHolder 并做 measure/layout,以便快速滑动时这些 item 已就绪,减少卡顿。 |
| 设置方式 | 例如:(recyclerView.layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4,表示在垂直列表中会预取接下来约 4 个 item 的 ViewHolder。 |
| 与“数据预加载”区别 | 这里是 ViewHolder 的预取(提前 measure/layout),不涉及网络或数据源;“数据预加载”指在滑到底前提前请求下一页数据(见 9.3),二者可同时使用。 |
14.9 列表卡顿的排查思路
| 排查方向 | 说明 |
|---|---|
| 是否用好了 ViewHolder | 在 ViewHolder 中缓存子 View 引用,避免在 onBind 里重复 findViewById。 |
| onBindViewHolder 是否过重 | 只做数据赋值与简单 UI 更新,不做复杂计算、网络请求、大对象创建;若有耗时逻辑,放到后台或预计算。 |
| 布局是否过深 | item 布局层级过深会拉长 measure/layout 时间,尽量扁平化(如 ConstraintLayout)。 |
| 图片是否异步与回收清理 | 用 Glide/Coil 等异步加载,滑动时可暂停、回收时 clear,见 14.7。 |
| 是否滥用 notifyDataSetChanged | 尽量用局部 notify 或 DiffUtil,避免整表刷新导致所有可见 item 重绑。 |
| 用工具定位 | 用 Systrace、Android Profiler 抓主线程与 onBind/onCreate 耗时,见 14.10、十二章。 |
可与 6.1 优化手段对照使用。
14.10 性能与问题排查常用工具
与十二章第三方库与工具对应,此处侧重排查卡顿与泄漏时的用法。
| 工具 | 用途 | 简要用法 |
|---|---|---|
| Android Profiler | 看 CPU、内存、网络占用 | 在 Android Studio 中打开 Profiler,选择进程,录制一段时间内的 CPU/内存,结合滑动列表复现卡顿或内存增长,查看主线程和 Adapter 相关方法耗时。 |
| Systrace | 看帧率、主线程耗时与调用栈 | 在 onBindViewHolder、onCreateViewHolder 等关键路径加 Trace.beginSection("xxx") / endSection(),用 systrace 录制后查看这些区间,定位卡顿发生的具体方法。 |
| Layout Inspector | 看运行时 View 层级与属性 | 连接设备、选择进程后查看当前界面 View 树,检查 item 布局嵌套深度和宽高是否合理。 |
| LeakCanary | 检测内存泄漏 | 接入后若 Activity/Fragment 等被泄漏会给出引用链,便于排查 Adapter/ViewHolder 或异步回调导致的泄漏。 |
在 Adapter 关键方法内打 Log 或 Trace 可进一步精确定位是哪一步耗时。
14.11 NO_POSITION 的含义与正确用法
| 项目 | 说明 |
|---|---|
| 含义 | RecyclerView.NO_POSITION 是常量 -1,表示当前 ViewHolder 没有有效的 adapter position。例如:ViewHolder 已从 RecyclerView 上移除、正在回收、或处于动画过程中的中间状态,此时用 holder.adapterPosition 会得到 RecyclerView.NO_POSITION(即 -1)。 |
| 为何会出现 | 在点击、长按等回调执行时,若用户操作很快或列表正在刷新,该 ViewHolder 可能已被回收或 position 已失效,若仍用旧的 position 去访问数据会越界或错乱。 |
| 正确用法 | 在回调里用 holder.adapterPosition 取位置,并先判断 if (pos != RecyclerView.NO_POSITION) 再访问 items[pos] 或执行后续逻辑,避免崩溃和数据错乱。 |
示例:
holder.itemView.setOnClickListener {
val pos = holder.adapterPosition
if (pos != RecyclerView.NO_POSITION) {
val item = items[pos]
onItemClick(item)
}
}
14.12 数据源与 notify 的线程安全
| 要点 | 说明 |
|---|---|
| 主线程更新 UI 与 notify | 数据列表的修改(增删改)以及调用 notifyXXX 都应在主线程执行,因为 notify 会触发布局和绘制,必须在 UI 线程。若新数据在后台线程算好,应切回主线程再改数据并 notify(或先用 DiffUtil 在后台算 diff,再在主线程 dispatchUpdatesTo(adapter))。 |
| 避免并发改同一数据源 | 若多线程可能同时修改同一列表,需加锁或使用并发安全集合,且更新完数据后的 notify 仍须在主线程;不要在一个线程改数据、另一个线程 notify,容易 IndexOutOfBounds 或状态不一致。 |
| DiffUtil 的线程 | DiffUtil.calculateDiff() 可放在后台线程执行(计算可能较重),但 diffResult.dispatchUpdatesTo(adapter) 必须在主线程调用。 |