RecyclerView 完全指南

129 阅读34分钟

一、基础概念

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 的区别

对比项RecyclerViewListView
布局管理通过 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 的区别

对比项onCreateViewHolderonBindViewHolder
调用时机需要新的 ViewHolder 时每次要显示/更新该 item 时
调用频率相对少(复用为主)频繁(滑动、刷新都会触发)
主要职责创建 View + ViewHolder把数据填到已有 View 上
性能关注点布局 inflate、创建开销绑定逻辑要轻量,避免耗时操作

建议:onCreateViewHolder 中做一次性初始化;在 onBindViewHolder 中只做数据设置,不做复杂计算或网络请求。


3.3 如何实现点击事件(含长按)

RecyclerView 没有内置 OnItemClickListener,需在 Adapter 内为 itemView 或子 View 设置监听。

方式传参形式核心写法优点适用场景
Lambda 回调构造函数传入 (T) -> Unitholder.itemView.setOnClickListener { ... },回调内用 holder.adapterPosition 取位置并判断 != RecyclerView.NO_POSITION 再访问数据写法简单、调用处直观仅需点击、逻辑简单(推荐)
接口回调构造函数传入 OnItemClickListenerholder.itemView.setOnClickListener { ... },回调中取数据时同样建议用 holder.adapterPosition 并判断 != RecyclerView.NO_POSITION,避免 position 失效便于多实现、可复用接口多处复用同一回调、需要接口抽象时
点击 + 长按两个 Lambda:(Int, T) -> Unit(Int, T) -> Boolean同时设置 setOnClickListenersetOnLongClickListener,长按返回 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    │                    │  12  │                │  12  │
├─────────┤                    ├─────┼─────┤                │     ├─────┤
│    2    │                    │  34  │                │     │  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典型用途
1mAttachedScrap当前仍属本 RV、未进 Pool 的 ViewHolder(局部刷新时被临时从视图树摘下)屏内可见的若干项是(局部刷新时仅对该项 rebind)notifyItemChanged 等局部刷新
2mCachedViews刚滑出屏幕的 ViewHolder默认 2(由系统决定)同 position 复用可不用,否则需 rebind快速来回滑动时复用
3ViewCacheExtension由开发者自定义自定义自定义自定义特殊缓存策略,一般不用
4RecycledViewPool按 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) 显示

文字简述(与流程图对应):

  1. 初始: 屏幕显示 item 0~9,对应 10 个 ViewHolder,都在 RV 上 attach。
  2. 向下滑: item 0 滑出 → 进 mCachedViews;item 1 滑出 → 进 mCachedViews;item 2 滑出时 mCachedViews 已满(默认 2 个),item 0 的 ViewHolder 移入 RecycledViewPool(清空数据),item 2 的 ViewHolder 进 mCachedViews。
  3. 需要显示新 item: 要显示 position 10 时,按流程图 2 的顺序查找,最终从 RecycledViewPool 按 viewType 取 ViewHolder(或新建),再 onBindViewHolder(holder, 10) 后 measure、layout 显示。
  4. 向上滑回: position 0 再次进屏时,从 Pool 取同 type 的 ViewHolder,执行 onBindViewHolder(holder, 0) 后显示,实现“空壳”复用。

整体上:一、二级保留数据、优先复用;四级做跨屏/跨列表的“空壳”复用,必须 rebind。 理解这一点,就能说清 RecyclerView 的缓存机制。


六、性能优化

6.1 常见优化手段汇总

序号优化手段说明
1ViewHolder + 缓存子 View 引用在 ViewHolder 中缓存 findViewById 结果,避免在 onBindViewHolder 里重复查找,减轻滑动时的开销。
2setHasFixedSize(true)列表宽高固定时设置,数据变化时跳过对 RecyclerView 自身的重新测量,见 6.2。
3DiffUtil 增量更新用 DiffUtil 计算差异后 dispatchUpdatesTo,避免整表 notifyDataSetChanged,保留动画、减少 rebind,见 6.3。
4onBindViewHolder 只做轻量操作仅做数据赋值与简单逻辑,不在其中做复杂计算、网络请求或同步 IO,保证每帧耗时可控。
5布局扁平化减少 item 布局层级,优先用 ConstraintLayout 等减少嵌套,降低 measure/layout 成本。
6图片异步加载 + 滑动控制用 Glide/Coil 等异步加载图片;滑动时可暂停请求、停止后恢复,并在 onViewRecycled 中取消加载,见 14.7。
7initialPrefetchItemCount 预取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 会在布局时测量每条高度,无需额外配置。
setHasFixedSizesetHasFixedSize(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 列表为空的常见原因

原因说明与处理
未设置 LayoutManagerRecyclerView 必须设置 layoutManager 才会排版子 View。处理:recyclerView.layoutManager = LinearLayoutManager(this) 等。
数据源为空Adapter 的 getItemCount() 返回 0 时不会显示任何 item。处理:确认数据列表已赋值、未清空,且 getItemCount() 返回列表 size。
Item 根布局或关键 View 高度为 0xml 里 layout_height="0dp" 且约束错误,或权重/比例导致高度为 0。处理:检查 item 布局的宽高和约束,保证可见区域有有效高度。
RecyclerView 高度为 wrap_content 且无约束在 ScrollView、ConstraintLayout 等嵌套下,RecyclerView 可能被测量为 0。处理:改为 match_parent、固定高度,或配合 layout_constraintHeight_min 等给出最小高度。

10.2 数据更新导致的异常

异常/现象原因与处理
IndexOutOfBoundsExceptionnotifyXXX 的 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 要 ViewLayoutManager 在 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 等,提供 getViewForPositionrecycleView 等接口,LayoutManager 只通过 Recycler 取 View、还 View,不直接管理缓存结构。
  • Adapter:提供 getItemCount()getItemViewType(position)onCreateViewHolderonBindViewHolder;Recycler 在需要新 ViewHolder 时调 onCreate,在需要绑定数据时调 onBind。
  • LayoutManager:只负责“要哪个 position、摆在哪”;“从哪拿 View、还到哪”交给 Recycler,“长什么样、绑什么数据”交给 Adapter。

把这三者分清,就能把“源码与机制”讲清楚:测量与布局以 LayoutManager 为核心,复用以 Recycler 为核心,数据与 View 以 Adapter 为核心。


十二、第三方库与工具

库 / 工具类型用途说明
Epoxy第三方库简化 RecyclerView Adapter、多类型列表用注解和 Model 构建列表,自动做 Diff 计算与更新,适合复杂多类型、多状态列表,减少手写 Adapter/ViewHolder 和 notify 逻辑。
Systrace系统工具分析列表滑动卡顿、主线程耗时onBindViewHolderonCreateViewHolder 等关键路径加 Trace.beginSection("xxx") / endSection(),用 systrace 录制后查看这些区间的耗时与主线程占用,定位卡顿原因。
Layout InspectorAndroid Studio 工具查看运行时 View 层级与属性连接设备后选择进程,可查看当前界面的 View 树、每个 View 的宽高和 margin 等,用于确认 item 布局嵌套深度、测量是否异常。
Android ProfilerAndroid 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.itemViewholder.xxxView 调用 setOnClickListener / setOnLongClickListener,在回调中根据 position(建议用 holder.adapterPosition 并判断 != RecyclerView.NO_POSITION)取数据并处理。
事件传递若子 View 消费了点击(返回 true),则 item 的点击不会触发;若需要“点 item 跳详情、点按钮做操作”,可分别给 itemView 和 button 设置监听,或在回调里根据 view.id 区分。
避免错位不要用 onBindViewHolderposition 参数闭包进回调,应用 holder.adapterPosition 并在使用前判断 != RecyclerView.NO_POSITION,见 14.11

14.7 列表中的图片加载与滑动卡顿

列表里大量图片若在主线程解码或滑动时仍全量加载,容易造成卡顿。

要点说明
使用异步库使用 Glide、Coil 等库在后台线程解码,并做好内存与磁盘缓存,避免主线程解码和重复请求。
滑动时暂停addOnScrollListeneronScrollStateChanged 中,若 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) 必须在主线程调用。