一、基础概念
1.1 什么是 RecyclerView?
答案:
RecyclerView 是 Android 提供的一个用于高效显示大量数据集合的视图组件。它是 ListView 的升级版本,提供了更灵活、更强大的功能。
主要作用:
- 显示列表数据:以列表、网格或瀑布流的形式展示数据
- 视图复用:通过 ViewHolder 模式复用视图,提高性能
- 灵活布局:支持多种布局方式(线性、网格、瀑布流等)
- 动画支持:内置动画支持,可以轻松实现增删改动画
基本使用示例:
kotlin
// 布局文件
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// Activity 中
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 属性简单设置 |
| 性能 | 多级缓存机制,性能更优 | 缓存机制相对简单 |
| 灵活性 | 高度可定制,支持自定义 LayoutManager | 定制性较差 |
| 点击事件 | 需要手动实现 | 内置 onItemClickListener |
代码对比示例:
kotlin
// ListView:内置点击事件
listView.setOnItemClickListener { ... }
// RecyclerView:需要手动实现点击事件
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter(data)
为什么 RecyclerView 更好?
- 性能更优:多级缓存机制,滑动更流畅
- 更灵活:可以轻松切换不同的布局方式
- 更现代:Google 推荐使用,ListView 已不再更新
1.3 RecyclerView 的四大核心组件
答案:
RecyclerView 的四大核心组件是:
- RecyclerView:容器本身,负责显示和管理子视图
- Adapter:数据适配器,负责将数据绑定到视图
- LayoutManager:布局管理器,负责子视图的排列方式
- ViewHolder:视图持有者,缓存视图引用,提高性能
组件关系图:
scss
RecyclerView (容器)
├── LayoutManager (决定如何排列)
├── Adapter (决定显示什么数据)
│ └── ViewHolder (缓存视图引用)
└── ItemAnimator (动画效果,可选)
代码示例:
kotlin
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this) // LayoutManager
recyclerView.adapter = MyAdapter(dataList) // Adapter
recyclerView.itemAnimator = DefaultItemAnimator() // ItemAnimator(可选)
// ViewHolder 在 Adapter 中创建
二、ViewHolder 机制
2.1 什么是 ViewHolder?
答案:
ViewHolder 模式是一种设计模式,用于缓存视图组件的引用,避免重复调用 findViewById(),从而提高列表滚动的性能。
核心思想:
- 将视图引用存储在 ViewHolder 中
- 视图创建时查找一次,后续直接复用
- 减少 findViewById 的调用次数
代码示例:
kotlin
// ❌ 错误:每次都调用 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 的优势
答案:
ViewHolder 机制的优势主要体现在性能提升上:
1. 减少 findViewById 调用
kotlin
// 不使用 ViewHolder:每次绑定都调用 findViewById
// 假设有 1000 个 item,每个 item 有 5 个 View
// 需要调用 findViewById 5000 次!
// 使用 ViewHolder:只在创建时调用一次
// 1000 个 item,每个 item 有 5 个 View
// 只需要调用 findViewById 5000 次(创建时),但可以复用!
2. 减少内存分配
- 视图引用被缓存,不需要重复创建
- 减少 GC(垃圾回收)压力
3. 提高滑动流畅度
- findViewById 是耗时操作
- 减少调用次数,滑动更流畅
- 避免在滑动时频繁查找视图
性能对比示例:
kotlin
// 性能测试代码
class PerformanceTest {
fun testWithoutViewHolder() {
val startTime = System.currentTimeMillis()
for (i in 0..1000) {
val textView = view.findViewById<TextView>(R.id.textView) // 耗时
}
val endTime = System.currentTimeMillis()
println("不使用 ViewHolder: ${endTime - startTime}ms")
}
fun testWithViewHolder() {
val holder = ViewHolder(view)
val startTime = System.currentTimeMillis()
for (i in 0..1000) {
holder.textView.text = "text" // 直接使用,快速
}
val endTime = System.currentTimeMillis()
println("使用 ViewHolder: ${endTime - startTime}ms")
}
}
2.3 如何创建自定义 ViewHolder
答案:
创建自定义 ViewHolder 的步骤:
代码示例:
kotlin
// ViewHolder:缓存视图引用
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
fun bind(data: MyData) {
textView.text = data.text
}
}
// Adapter:使用 ViewHolder
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 Adapter 必须实现哪些方法
答案:
RecyclerView.Adapter 必须实现三个核心方法:
1. onCreateViewHolder() - 创建 ViewHolder
kotlin
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// 创建并返回 ViewHolder
}
2. onBindViewHolder() - 绑定数据到 ViewHolder
kotlin
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 将数据绑定到 ViewHolder 的视图上
}
3. getItemCount() - 返回数据总数
kotlin
override fun getItemCount(): Int {
// 返回数据列表的大小
}
完整示例:
kotlin
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)
}
}
方法调用时机:
onCreateViewHolder():当需要创建新的 ViewHolder 时调用(视图复用池中没有可用的)onBindViewHolder():当需要将数据绑定到 ViewHolder 时调用(每次显示 item 时)getItemCount():RecyclerView 需要知道有多少个 item 时调用
3.2 onCreateViewHolder 和 onBindViewHolder 的区别
答案:
这两个方法在 RecyclerView 中扮演不同的角色:
| 对比项 | onCreateViewHolder | onBindViewHolder |
|---|---|---|
| 调用时机 | 创建新的 ViewHolder 时 | 绑定数据到 ViewHolder 时 |
| 调用频率 | 较少(只在需要新 ViewHolder 时) | 频繁(每次显示 item 时) |
| 主要作用 | 创建视图和 ViewHolder | 更新视图内容 |
| 性能影响 | 影响创建性能 | 影响滚动性能 |
详细说明:
onCreateViewHolder() - 创建阶段
kotlin
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ViewHolder(view) // 只在需要新 ViewHolder 时调用
}
onBindViewHolder() - 绑定阶段
kotlin
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position] // 每次显示 item 时调用
}
性能优化建议:
kotlin
// ✅ onCreateViewHolder:做一次性初始化
override fun onCreateViewHolder(...): ViewHolder {
return ViewHolder(...)
}
// ✅ onBindViewHolder:只更新数据,不做耗时操作
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position]
// ❌ 不要做复杂计算或网络请求
}
3.3 如何实现点击事件
答案:
RecyclerView 不像 ListView 那样有内置的点击监听器,需要手动实现。有几种方式:
方式 1:使用 Lambda(推荐)
kotlin
class MyAdapter(
private val items: List<String>,
private val onItemClick: (String) -> Unit
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position]
holder.itemView.setOnClickListener {
onItemClick(items[position])
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
}
}
// 使用
val adapter = MyAdapter(dataList) { item ->
Toast.makeText(this, "点击: $item", Toast.LENGTH_SHORT).show()
}
方式 3:长按事件
kotlin
class MyAdapter(
private val items: List<String>,
private val onItemClick: (Int, String) -> Unit,
private val onItemLongClick: (Int, String) -> Boolean = { _, _ -> false }
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: String, position: Int) {
itemView.setOnClickListener {
onItemClick(position, item)
}
itemView.setOnLongClickListener {
onItemLongClick(position, item)
}
}
}
}
3.4 如何实现多类型视图
答案:
多类型视图是指在一个 RecyclerView 中显示不同类型的 item 布局。
实现步骤:
代码示例:
kotlin
// 1. 定义视图类型
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1
}
// 2. 返回视图类型
override fun getItemViewType(position: Int): Int {
return if (position == 0) TYPE_HEADER else TYPE_ITEM
}
// 3. 根据类型创建 ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return when (viewType) {
TYPE_HEADER -> HeaderViewHolder(...)
TYPE_ITEM -> ItemViewHolder(...)
else -> throw IllegalArgumentException()
}
}
// 4. 绑定数据
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.bind(...)
is ItemViewHolder -> holder.bind(...)
}
}
3.5 notifyDataSetChanged() 的优缺点
答案:
notifyDataSetChanged() 是最简单的数据更新方法,但存在明显的性能问题。
优点:
- 使用简单:一行代码即可刷新整个列表
- 无需计算:不需要知道具体哪些数据变化了
缺点:
- 性能差:会刷新所有可见的 item,即使数据没有变化
- 丢失动画:无法显示增删改的动画效果
- 用户体验差:可能导致闪烁、滚动位置丢失等问题
代码示例:
kotlin
// ❌ 不推荐:刷新所有 item
fun updateData(newItems: List<String>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged() // 性能差,刷新所有
}
// ✅ 推荐:局部更新
fun addItem(item: String) {
items.add(item)
notifyItemInserted(items.size - 1) // 只刷新新增的
}
fun removeItem(position: Int) {
items.removeAt(position)
notifyItemRemoved(position) // 只刷新删除的
}
性能对比:
notifyDataSetChanged():刷新所有 item,耗时约 100ms(1000 个 item)notifyItemChanged(5):只刷新第 5 个 item,耗时约 1ms
最佳实践:
kotlin
// ✅ 使用 DiffUtil(推荐)
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
private var items = mutableListOf<String>()
fun updateData(newItems: List<String>) {
val diffResult = DiffUtil.calculateDiff(
MyDiffCallback(items, newItems)
)
items = newItems.toMutableList()
diffResult.dispatchUpdatesTo(this) // 智能更新,性能最优
}
}
四、LayoutManager
4.1 LayoutManager 的作用
答案:
LayoutManager 负责决定 RecyclerView 中的 item 如何排列和显示。
主要职责:
- 测量子视图:计算每个 item 的大小
- 布局子视图:决定每个 item 的位置
- 回收视图:管理视图的回收和复用
代码示例:
kotlin
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
// LinearLayoutManager - 线性布局(垂直或水平)
recyclerView.layoutManager = LinearLayoutManager(this) // 默认垂直
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) // 水平
// GridLayoutManager - 网格布局
recyclerView.layoutManager = GridLayoutManager(this, 3) // 3 列
// StaggeredGridLayoutManager - 瀑布流布局
recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) // 2 列垂直
4.2 支持哪些 LayoutManager
答案:
RecyclerView 内置了三种常用的 LayoutManager:
1. LinearLayoutManager - 线性布局
kotlin
// 垂直列表(默认)
val linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
// 水平列表
val horizontalLayoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.layoutManager = horizontalLayoutManager
// 反向列表
val reverseLayoutManager = LinearLayoutManager(
this,
LinearLayoutManager.VERTICAL,
true // 反向
)
recyclerView.layoutManager = reverseLayoutManager
2. GridLayoutManager - 网格布局
kotlin
// 2 列网格
val gridLayoutManager = GridLayoutManager(this, 2)
recyclerView.layoutManager = gridLayoutManager
// 3 列网格,支持不同 item 占不同列数
val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position) {
0 -> 3 // 第一个 item 占 3 列(全宽)
else -> 1 // 其他 item 占 1 列
}
}
}
gridLayoutManager.spanSizeLookup = spanSizeLookup
3. StaggeredGridLayoutManager - 瀑布流布局
kotlin
// 2 列垂直瀑布流
val staggeredLayoutManager = StaggeredGridLayoutManager(
2, // 列数
StaggeredGridLayoutManager.VERTICAL // 方向
)
recyclerView.layoutManager = staggeredLayoutManager
// 3 行水平瀑布流
val horizontalStaggered = StaggeredGridLayoutManager(
3, // 行数
StaggeredGridLayoutManager.HORIZONTAL // 方向
)
recyclerView.layoutManager = horizontalStaggered
布局效果对比:
scss
LinearLayoutManager (垂直):
┌─────┐
│ 1 │
├─────┤
│ 2 │
├─────┤
│ 3 │
└─────┘
GridLayoutManager (2列):
┌─────┬─────┐
│ 1 │ 2 │
├─────┼─────┤
│ 3 │ 4 │
└─────┴─────┘
StaggeredGridLayoutManager (2列):
┌─────┬─────┐
│ 1 │ 2 │
│ ├─────┤
│ │ 3 │
├─────┤ │
│ 4 │ │
└─────┴─────┘
五、缓存机制
5.1 RecyclerView 的缓存机制
答案:
RecyclerView 采用四级缓存机制,这是其高性能的核心。
四级缓存结构:
RecyclerView 缓存机制
├── 一级缓存:mAttachedScrap(屏幕内缓存)
├── 二级缓存:mCachedViews(屏幕外缓存)
├── 三级缓存:ViewCacheExtension(自定义缓存,可选)
└── 四级缓存:RecycledViewPool(回收池)
详细说明:
1. 一级缓存(mAttachedScrap)
- 作用:存储当前屏幕内可见的 ViewHolder
- 特点:数据未变化时直接复用,无需重新绑定
- 场景:数据局部更新时使用
kotlin
// 当调用 notifyItemChanged(5) 时
// 第 5 个 item 会先放入 mAttachedScrap
// 然后重新绑定数据,放回原位置
2. 二级缓存(mCachedViews)
- 作用:存储刚滑出屏幕的 ViewHolder
- 特点:默认最多缓存 2 个,数据未变化
- 场景:快速来回滑动时复用
kotlin
// 用户向下滑动,item 1 滑出屏幕
// item 1 的 ViewHolder 放入 mCachedViews
// 如果用户立即向上滑动,可以直接复用
3. 三级缓存(ViewCacheExtension)
- 作用:开发者自定义的缓存层
- 特点:可选,大多数情况下不需要
- 场景:特殊缓存需求
4. 四级缓存(RecycledViewPool)
- 作用:存储所有类型的 ViewHolder
- 特点:数据已清空,需要重新绑定
- 场景:跨 RecyclerView 共享,或作为最后备选
缓存查找顺序:
kotlin
// RecyclerView 需要 ViewHolder 时的查找顺序:
1. 查找 mAttachedScrap(一级缓存)
↓ 未找到
2. 查找 mCachedViews(二级缓存)
↓ 未找到
3. 查找 ViewCacheExtension(三级缓存,如果有)
↓ 未找到
4. 查找 RecycledViewPool(四级缓存)
↓ 未找到
5. 创建新的 ViewHolder(调用 onCreateViewHolder)
缓存流程示例:
首次显示:创建 10 个 ViewHolder
向下滑动:item 0 滑出 → 放入 mCachedViews
向上滑动:item 0 从 mCachedViews 直接复用(无需重新绑定)
继续滑动:mCachedViews 满 → 移入 RecycledViewPool
5.2 一级缓存(mAttachedScrap)的作用
答案:
mAttachedScrap 是 RecyclerView 的第一级缓存,用于存储当前屏幕内可见的 ViewHolder。
主要作用:
- 局部更新优化 :当调用
notifyItemChanged()时,避免重新创建 ViewHolder - 快速复用:数据未变化时直接复用,无需重新绑定
- 保持状态:保持 ViewHolder 的选中状态、动画状态等
工作原理:
markdown
更新 item 5:
1. ViewHolder 放入 mAttachedScrap
2. 调用 onBindViewHolder 重新绑定
3. ViewHolder 取出复用
结果:ViewHolder 复用,只更新数据
代码示例:
kotlin
fun updateItem(position: Int, newText: String) {
items[position] = newText
notifyItemChanged(position) // ViewHolder 复用,只更新数据
}
优势:
- 性能好:不需要重新创建 View
- 保持状态:保持用户交互状态(如选中、展开等)
- 流畅:更新过程更平滑
5.3 二级缓存(mCachedViews)的作用
答案:
mCachedViews 存储刚滑出屏幕的 ViewHolder,用于快速来回滑动时的复用。
主要特点:
- 默认容量:最多缓存 2 个 ViewHolder
- 数据完整:ViewHolder 中的数据未清空
- 快速复用:可以直接使用,无需重新绑定
工作原理:
向下滑动:item 0 滑出 → 放入 mCachedViews(数据保留)
向上滑动:item 0 从 mCachedViews 直接复用(无需重新绑定)
继续滑动:mCachedViews 满(2个)→ 移入 RecycledViewPool
为什么只缓存 2 个?
- 平衡内存和性能
- 大多数情况下,用户来回滑动不会超过 2 个 item
- 如果缓存太多,会占用过多内存
5.4 三级缓存(ViewCacheExtension)的作用
答案:
ViewCacheExtension 是 RecyclerView 的第三级缓存,是开发者可以自定义的缓存层。
主要特点:
- 可选缓存:大多数情况下不需要使用
- 自定义实现:开发者可以自定义缓存逻辑
- 特殊场景:适用于有特殊缓存需求的场景
使用场景:
- 需要特殊的缓存策略
- 需要跨 RecyclerView 共享特定类型的 ViewHolder
- 需要自定义缓存的生命周期
代码示例:
kotlin
class CustomViewCacheExtension : RecyclerView.ViewCacheExtension() {
private val cache = mutableMapOf<Int, RecyclerView.ViewHolder>()
override fun getViewForPositionAndType(
recycler: RecyclerView.Recycler,
position: Int,
viewType: Int
): View? {
// 自定义缓存逻辑
return cache[viewType]?.itemView
}
}
// 使用
recyclerView.setViewCacheExtension(CustomViewCacheExtension())
注意: 大多数情况下不需要使用,默认的缓存机制已经足够。
5.5 四级缓存(RecycledViewPool)的作用
答案:
RecycledViewPool 是 RecyclerView 的最后一级缓存,存储所有被回收的 ViewHolder。
主要特点:
- 数据已清空:ViewHolder 中的数据已被清空
- 需要重新绑定 :使用时需要调用
onBindViewHolder - 可共享:多个 RecyclerView 可以共享同一个 Pool
- 按类型存储:不同类型的 ViewHolder 分开存储
工作原理:
kotlin
// ViewHolder 进入 RecycledViewPool 的流程:
// 1. ViewHolder 从 mCachedViews 移出
// 2. 清空 ViewHolder 中的数据(调用 onViewRecycled)
// 3. 放入 RecycledViewPool
// 使用时:
// 1. 从 RecycledViewPool 获取 ViewHolder
// 2. 调用 onBindViewHolder 重新绑定数据
// 3. 显示在屏幕上
代码示例:共享 RecycledViewPool
kotlin
val sharedPool = RecyclerView.RecycledViewPool()
sharedPool.setMaxRecycledViews(0, 20)
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool) // 共享缓存
优势:
- 内存优化:多个 RecyclerView 共享缓存,减少内存占用
- 性能提升:避免重复创建 ViewHolder
- 灵活性:可以自定义缓存数量
六、性能优化
6.1 如何优化性能
答案:
RecyclerView 性能优化是一个综合性的工作,需要从多个方面入手。
优化策略:
kotlin
// 1. 使用 ViewHolder 缓存视图引用
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
}
// 2. 设置固定大小
recyclerView.setHasFixedSize(true)
// 3. 使用 DiffUtil 增量更新
val diffResult = DiffUtil.calculateDiff(MyDiffCallback(oldItems, newItems))
diffResult.dispatchUpdatesTo(adapter)
// 4. 避免在 onBindViewHolder 中做耗时操作
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position].text // ✅ 只更新视图
// ❌ 不要做复杂计算或网络请求
}
// 5. 使用 ConstraintLayout 减少布局嵌套
// ❌ LinearLayout 嵌套 → ✅ ConstraintLayout
// 6. 图片加载使用异步库
Glide.with(context).load(url).into(imageView)
// 7. 设置预加载
layoutManager.initialPrefetchItemCount = 4
6.2 setHasFixedSize(true) 的作用
答案:
setHasFixedSize(true) 告诉 RecyclerView,它的尺寸是固定的,不会因为内容变化而改变大小。
作用:
- 优化布局计算:RecyclerView 知道大小不变,可以跳过一些布局测量
- 提高性能:减少不必要的布局重新计算
使用场景:
kotlin
// ✅ 适合使用:RecyclerView 的大小固定
<androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
recyclerView.setHasFixedSize(true) // 大小不会改变
// ❌ 不适合使用:RecyclerView 的大小可能改变
<LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
recyclerView.setHasFixedSize(false) // 大小可能改变
代码示例:
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
// 如果 RecyclerView 的宽高是 match_parent 或固定值
// 设置此属性可以优化性能
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter(dataList)
}
}
性能影响:
- 设置
true后,当数据变化时,RecyclerView 不会重新测量自己的大小 - 可以节省布局计算时间,特别是在数据频繁更新时
6.3 什么是 DiffUtil?如何使用
答案:
DiffUtil 是 Android 提供的一个工具类,用于计算两个列表之间的差异,并生成更新操作。
主要优势:
- 增量更新:只更新变化的部分,而不是整个列表
- 自动动画:配合 RecyclerView 可以自动显示增删改动画
- 性能优化:避免不必要的视图刷新
使用方法:
代码示例:
kotlin
// 1. 创建 DiffUtil.Callback
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 // 判断是否是同一个 item
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos] // 判断内容是否相同
}
}
// 2. 在 Adapter 中使用
fun updateData(newItems: List<Item>) {
val diffResult = DiffUtil.calculateDiff(MyDiffCallback(items, newItems))
items = newItems.toMutableList()
diffResult.dispatchUpdatesTo(this) // 只更新变化的部分
}
性能对比:
notifyDataSetChanged():刷新所有 item,耗时约 100ms(1000 个 item)DiffUtil:只刷新变化的 item,耗时约 1ms
七、ItemDecoration
7.1 ItemDecoration 的作用
答案:
ItemDecoration 用于在 RecyclerView 的 item 之间添加装饰效果,如分割线、间距、背景等。
主要作用:
- 添加分割线:在 item 之间绘制分割线
- 设置间距:为 item 添加内边距或外边距
- 绘制背景:为 item 添加背景装饰
- 自定义装饰:实现复杂的装饰效果
基本使用:
kotlin
// 添加分割线
val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
7.2 如何自定义 ItemDecoration
答案:
自定义 ItemDecoration 需要继承 RecyclerView.ItemDecoration 并重写相应方法。
核心方法:
- getItemOffsets() - 设置 item 的偏移量(为装饰留出空间)
- onDraw() - 在 item 下方绘制装饰
- onDrawOver() - 在 item 上方绘制装饰(详见 7.3)
代码示例:自定义分割线
kotlin
class CustomDividerDecoration(
private val height: Int,
private val color: Int
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
// 为分割线留出空间(最后一个 item 不需要)
if (parent.getChildAdapterPosition(view) != parent.adapter!!.itemCount - 1) {
outRect.bottom = height
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val paint = Paint().apply { this.color = color }
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (parent.getChildAdapterPosition(child) != parent.adapter!!.itemCount - 1) {
val top = child.bottom.toFloat()
c.drawRect(0f, top, parent.width.toFloat(), top + height, paint)
}
}
}
}
// 使用方式
recyclerView.addItemDecoration(CustomDividerDecoration(2.dp, Color.GRAY))
其他常见用法:
- 设置间距 :在
getItemOffsets()中设置outRect.set(spacing, spacing, spacing, spacing) - 复杂装饰 :结合
onDraw()和onDrawOver()实现悬浮效果等
7.3 onDraw() 和 onDrawOver() 的区别
答案:
这两个方法用于在不同层级绘制装饰。
区别说明:
| 对比项 | onDraw() | onDrawOver() |
|---|---|---|
| 绘制位置 | 在 item 下方绘制 | 在 item 上方绘制 |
| 绘制顺序 | 先绘制 | 后绘制 |
| 使用场景 | 分割线、背景 | 悬浮效果、遮罩 |
绘制顺序:
markdown
绘制顺序(从下到上):
1. RecyclerView 背景
2. onDraw() 绘制的内容(分割线等)
3. Item 视图
4. onDrawOver() 绘制的内容(悬浮效果等)
代码示例:
kotlin
class MultiLayerDecoration : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
// 在 item 下方绘制分割线
val paint = Paint().apply {
color = Color.GRAY
strokeWidth = 1f
}
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val bottom = child.bottom.toFloat()
c.drawLine(
child.left.toFloat(),
bottom,
child.right.toFloat(),
bottom,
paint
)
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
// 在 item 上方绘制悬浮效果(例如:选中高亮)
val paint = Paint().apply {
color = Color.parseColor("#33000000") // 半透明黑色
}
// 绘制选中项的高亮效果
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (isSelected(child)) {
c.drawRect(
child.left.toFloat(),
child.top.toFloat(),
child.right.toFloat(),
child.bottom.toFloat(),
paint
)
}
}
}
private fun isSelected(view: View): Boolean {
// 判断是否选中
return view.isSelected
}
}
八、ItemAnimator
8.1 ItemAnimator 的作用
答案:
ItemAnimator 负责处理 RecyclerView 中 item 的增删改动画效果。
主要作用:
- 添加动画:item 添加时的动画
- 删除动画:item 删除时的动画
- 移动动画:item 位置变化时的动画
- 更改动画:item 内容变化时的动画
默认动画:
kotlin
// RecyclerView 使用 DefaultItemAnimator 作为默认动画
recyclerView.itemAnimator = DefaultItemAnimator()
8.2 如何自定义 ItemAnimator
答案:
自定义 ItemAnimator 需要继承 RecyclerView.ItemAnimator 并实现相应方法。
核心方法:
- animateAdd() - 处理添加动画
- animateRemove() - 处理删除动画
- animateMove() - 处理移动动画
- animateChange() - 处理更改动画
代码示例:简单的淡入淡出动画
kotlin
class FadeItemAnimator : RecyclerView.ItemAnimator() {
override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.alpha = 0f
holder.itemView.animate()
.alpha(1f)
.setDuration(300)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
dispatchAddFinished(holder)
}
})
.start()
return true
}
override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.animate()
.alpha(0f)
.setDuration(300)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
dispatchRemoveFinished(holder)
}
})
.start()
return true
}
override fun animateMove(
holder: RecyclerView.ViewHolder,
fromX: Int, fromY: Int, toX: Int, toY: Int
): Boolean {
val deltaX = toX - fromX
val deltaY = toY - fromY
holder.itemView.translationX = -deltaX.toFloat()
holder.itemView.translationY = -deltaY.toFloat()
holder.itemView.animate()
.translationX(0f)
.translationY(0f)
.setDuration(300)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
dispatchMoveFinished(holder)
}
})
.start()
return true
}
override fun animateChange(
oldHolder: RecyclerView.ViewHolder,
newHolder: RecyclerView.ViewHolder,
fromLeft: Int, fromTop: Int, toLeft: Int, toTop: Int
): Boolean {
return false // 使用默认动画
}
override fun runPendingAnimations() {}
override fun endAnimation(item: RecyclerView.ViewHolder) {
item.itemView.clearAnimation()
}
override fun endAnimations() {}
override fun isRunning(): Boolean = false
}
// 使用方式
recyclerView.itemAnimator = FadeItemAnimator()
注意事项:
- 动画结束后必须调用
dispatchAddFinished()、dispatchRemoveFinished()等方法 - 可以返回
false表示使用默认动画 - 大多数情况下使用
DefaultItemAnimator即可满足需求
九、高级特性
9.1 如何处理动态高度的 Item
答案:
RecyclerView 默认支持动态高度的 item,但需要注意一些优化点。
基本使用:
kotlin
// 如果 item 高度是动态的,不需要特殊设置
// RecyclerView 会自动测量每个 item 的高度
class DynamicHeightAdapter(private val items: List<Item>) :
RecyclerView.Adapter<DynamicHeightAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_dynamic, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
// 设置内容,高度会自动调整
holder.titleTextView.text = item.title
holder.contentTextView.text = item.content // 内容长度不同,高度不同
}
override fun getItemCount() = items.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
val contentTextView: TextView = itemView.findViewById(R.id.contentTextView)
}
}
性能优化:
kotlin
// 如果所有 item 高度相同,设置固定高度可以优化性能
recyclerView.setHasFixedSize(false) // 动态高度必须为 false
// 使用 ConstraintLayout 可以更好地处理动态高度
// item_dynamic.xml
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/titleTextView"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/contentTextView"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
9.2 如何实现数据预加载
答案:
数据预加载是指监听滚动事件,在用户滑动到列表底部之前就开始加载更多数据,从而提升用户体验,避免用户等待数据加载。
实现方式:
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter(dataList)
// 监听滚动,提前加载数据
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
val totalItemCount = layoutManager.itemCount
// 距离底部还有 5 个 item 时开始预加载
if (lastVisiblePosition >= totalItemCount - 5) {
loadNextPage()
}
}
})
}
private fun loadNextPage() {
// 加载下一页数据
// 注意:需要防止重复加载
}
}
注意事项:
- 需要防止重复加载(使用标志位或锁)
- 预加载阈值建议设置为 3-5 个 item
- 对于网络请求,需要考虑取消机制
- ViewHolder 预加载见 14.11 initialPrefetchItemCount
十、常见问题
10.1 RecyclerView 显示空白的原因
答案:
RecyclerView 显示空白通常由以下原因导致:
常见原因及解决方案:
1. 没有设置 LayoutManager
kotlin
// ❌ 错误:没有设置 LayoutManager
recyclerView.adapter = adapter
// ✅ 正确:必须设置 LayoutManager
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
2. 数据为空
kotlin
// 检查数据是否为空
if (dataList.isEmpty()) {
// 显示空状态视图
showEmptyView()
} else {
recyclerView.adapter = MyAdapter(dataList)
}
3. Item 布局高度为 0
xml
<!-- ❌ 错误:高度为 0 -->
<TextView
android:layout_height="0dp" />
<!-- ✅ 正确:设置合适的高度 -->
<TextView
android:layout_height="wrap_content" />
4. RecyclerView 高度问题
xml
<!-- ❌ 错误:高度为 wrap_content 且没有内容 -->
<RecyclerView
android:layout_height="wrap_content" />
<!-- ✅ 正确:使用 match_parent 或固定高度 -->
<RecyclerView
android:layout_height="match_parent" />
5. Adapter 的 getItemCount() 返回 0
kotlin
// 检查 getItemCount() 是否正确
override fun getItemCount(): Int {
return items.size // 确保返回正确的数量
}
调试方法:
kotlin
// 添加日志调试
Log.d("RecyclerView", "ItemCount: ${adapter.itemCount}")
Log.d("RecyclerView", "DataSize: ${dataList.size}")
Log.d("RecyclerView", "LayoutManager: ${recyclerView.layoutManager}")
10.2 如何避免数据更新异常
答案:
数据更新时的异常通常是由于在错误的时机更新数据导致的。
常见异常及解决方案:
1. IndexOutOfBoundsException
kotlin
// ❌ 错误:在后台线程更新数据后直接通知
Thread {
items.add("new item")
notifyItemInserted(items.size - 1) // 可能崩溃
}.start()
// ✅ 正确:在主线程更新
Thread {
items.add("new item")
runOnUiThread {
notifyItemInserted(items.size - 1)
}
}.start()
// ✅ 或者使用 Handler
handler.post {
notifyItemInserted(items.size - 1)
}
2. 并发修改异常
kotlin
// ❌ 错误:在遍历时修改列表
for (item in items) {
if (shouldRemove(item)) {
items.remove(item) // ConcurrentModificationException
}
}
// ✅ 正确:先收集要删除的项,再删除
val toRemove = items.filter { shouldRemove(it) }
items.removeAll(toRemove)
notifyItemRangeRemoved(0, toRemove.size)
3. 位置不匹配异常
kotlin
// ✅ 正确:使用 adapterPosition 而不是 position 参数
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.setOnClickListener {
val adapterPosition = holder.adapterPosition
if (adapterPosition != RecyclerView.NO_POSITION) {
val item = items[adapterPosition] // 安全访问
}
}
}
最佳实践:
kotlin
class SafeAdapter(private val items: MutableList<String>) :
RecyclerView.Adapter<SafeAdapter.ViewHolder>() {
// 线程安全的数据更新
private val lock = Any()
fun addItem(item: String) {
synchronized(lock) {
val position = items.size
items.add(item)
notifyItemInserted(position)
}
}
fun removeItem(position: Int) {
synchronized(lock) {
if (position in 0 until items.size) {
items.removeAt(position)
notifyItemRemoved(position)
}
}
}
fun updateItem(position: Int, newItem: String) {
synchronized(lock) {
if (position in 0 until items.size) {
items[position] = newItem
notifyItemChanged(position)
}
}
}
}
10.3 如何避免 ViewHolder 内存泄漏
答案:
ViewHolder 中可能持有 Context、监听器等引用,需要避免内存泄漏。
常见泄漏场景及解决方案:
1. 持有 Activity Context
kotlin
// ❌ 错误:ViewHolder 持有 Activity 引用
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val context: Context = itemView.context // 如果是 Activity Context,可能泄漏
}
// ✅ 正确:使用 Application Context 或 itemView.context
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val context: Context = itemView.context.applicationContext
}
2. 未取消异步任务
kotlin
// ❌ 错误:异步任务未取消
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun loadImage(url: String) {
// 如果 ViewHolder 被回收,但任务还在执行,可能泄漏
loadImageAsync(url) { bitmap ->
imageView.setImageBitmap(bitmap)
}
}
}
// ✅ 正确:在 onViewRecycled 中取消任务
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var imageLoadJob: Job? = null
fun loadImage(url: String) {
imageLoadJob = lifecycleScope.launch {
val bitmap = loadImageAsync(url)
imageView.setImageBitmap(bitmap)
}
}
fun cancelLoad() {
imageLoadJob?.cancel()
}
}
// 在 Adapter 中
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.cancelLoad() // 取消任务
}
3. 未移除监听器
kotlin
// ✅ 正确:在 onViewRecycled 中移除监听器
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.removeListeners()
}
十一、源码分析
11.1 RecyclerView 的绘制流程
答案:
RecyclerView 的绘制流程遵循 Android 的标准绘制流程,但加入了视图复用机制。
绘制流程:
scss
1. onMeasure() - 测量阶段
├── 测量 RecyclerView 自身大小
├── 测量可见的 item
└── 计算总高度/宽度
2. onLayout() - 布局阶段
├── 调用 LayoutManager.onLayoutChildren()
├── 回收不可见的 ViewHolder
├── 复用或创建新的 ViewHolder
└── 布局可见的 item
3. onDraw() - 绘制阶段
├── 绘制 RecyclerView 背景
├── 绘制 ItemDecoration (onDraw)
├── 绘制 item 视图
└── 绘制 ItemDecoration (onDrawOver)
关键源码分析:
kotlin
// RecyclerView.onMeasure()
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
// 1. 调用 LayoutManager 测量
layoutManager?.let {
it.onMeasure(recycler, state, widthSpec, heightSpec)
} ?: super.onMeasure(widthSpec, heightSpec)
}
// RecyclerView.onLayout()
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 1. 分发布局
dispatchLayout()
}
private fun dispatchLayout() {
// 2. 调用 LayoutManager 布局
layoutManager?.onLayoutChildren(recycler, state)
}
// LayoutManager.onLayoutChildren()
override fun onLayoutChildren(recycler: Recycler, state: State) {
// 1. 回收所有视图
detachAndScrapAttachedViews(recycler)
// 2. 填充可见区域
fill(recycler, layoutState, state, false)
}
视图复用流程:
kotlin
// 1. 需要 ViewHolder 时,先从缓存获取
fun getViewForPosition(position: Int): View {
// 查找顺序:
// 1. mAttachedScrap (一级缓存)
// 2. mCachedViews (二级缓存)
// 3. ViewCacheExtension (三级缓存)
// 4. RecycledViewPool (四级缓存)
// 5. 创建新的 ViewHolder
}
// 2. 视图滑出屏幕时,放入缓存
fun recycleView(view: View) {
val holder = getChildViewHolder(view)
// 根据情况放入不同级别的缓存
recycler.recycleView(holder)
}
11.2 如何实现视图复用
答案:
视图复用是 RecyclerView 高性能的核心机制。
复用机制原理:
1. 视图回收(Recycle)
kotlin
// 当 item 滑出屏幕时
fun recycleView(view: View) {
val holder = getChildViewHolder(view)
// 1. 清除数据绑定
holder.unbind()
// 2. 根据情况放入缓存
if (holder.isRecyclable) {
// 放入 RecycledViewPool
recycledViewPool.putRecycledView(holder)
}
}
2. 视图获取(Get)
kotlin
// 需要 ViewHolder 时
fun getViewForPosition(position: Int): ViewHolder {
// 1. 从缓存池获取
val holder = recycledViewPool.getRecycledView(viewType)
if (holder != null) {
// 2. 重新绑定数据
adapter.onBindViewHolder(holder, position)
return holder
}
// 3. 缓存中没有,创建新的
return adapter.onCreateViewHolder(parent, viewType)
}
3. 缓存策略
kotlin
// RecycledViewPool 的实现
class RecycledViewPool {
private val scrapHeaps = SparseArray<ScrapData>()
fun putRecycledView(holder: ViewHolder) {
val viewType = holder.itemViewType
val scrapHeap = getScrapHeapForType(viewType)
scrapHeap.add(holder)
// 限制每个类型的缓存数量
if (scrapHeap.size > maxRecycledViews) {
scrapHeap.remove(0) // 移除最旧的
}
}
fun getRecycledView(viewType: Int): ViewHolder? {
val scrapHeap = getScrapHeapForType(viewType)
return scrapHeap.removeLastOrNull()
}
}
复用流程图:
markdown
用户滑动列表
↓
Item 滑出屏幕
↓
ViewHolder 被回收
↓
放入 RecycledViewPool
↓
新的 Item 需要显示
↓
从 RecycledViewPool 获取 ViewHolder
↓
重新绑定数据 (onBindViewHolder)
↓
显示在屏幕上
11.3 LayoutManager 的测量和布局流程
答案:
LayoutManager 负责测量和布局 RecyclerView 中的 item。
测量流程(Measure):
kotlin
// LinearLayoutManager.onMeasure()
override fun onMeasure(
recycler: Recycler,
state: State,
widthSpec: Int,
heightSpec: Int
): IntArray {
// 1. 获取测量模式
val widthMode = View.MeasureSpec.getMode(widthSpec)
val heightMode = View.MeasureSpec.getMode(heightSpec)
// 2. 测量可见的 item
var width = 0
var height = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChild(child, widthSpec, heightSpec)
width = max(width, child.measuredWidth)
height += child.measuredHeight
}
return intArrayOf(width, height)
}
布局流程(Layout):
kotlin
// LinearLayoutManager.onLayoutChildren()
override fun onLayoutChildren(recycler: Recycler, state: State) {
// 1. 回收所有已附加的视图
detachAndScrapAttachedViews(recycler)
// 2. 计算布局方向
val layoutDirection = if (reverseLayout) -1 else 1
// 3. 填充可见区域
var currentPosition = 0
var currentTop = paddingTop
while (currentTop < height - paddingBottom) {
// 3.1 获取或创建 ViewHolder
val holder = recycler.getViewForPosition(currentPosition)
// 3.2 添加视图
addView(holder.itemView)
// 3.3 测量视图
measureChildWithMargins(holder.itemView, 0, 0)
// 3.4 布局视图
val left = paddingLeft
val top = currentTop
val right = left + holder.itemView.measuredWidth
val bottom = top + holder.itemView.measuredHeight
layoutDecorated(holder.itemView, left, top, right, bottom)
// 3.5 更新位置
currentTop = bottom
currentPosition += layoutDirection
}
// 4. 回收不可见的视图
recycleViewsOutOfBounds(recycler)
}
关键方法说明:
detachAndScrapAttachedViews(): 回收所有已附加的视图getViewForPosition(): 获取指定位置的 ViewHolder(可能从缓存获取)addView(): 将视图添加到 RecyclerViewmeasureChildWithMargins(): 测量子视图(包含 margin)layoutDecorated(): 布局子视图(包含 decoration 的偏移)recycleViewsOutOfBounds(): 回收超出边界的视图
十二、第三方库与工具
12.1 Epoxy 库的作用和使用场景
答案:
Epoxy 是 Airbnb 开发的一个库,用于简化 RecyclerView 的 Adapter 开发。
主要优势:
- 简化代码:减少样板代码
- 类型安全:编译时检查
- 自动 Diff:自动计算差异并更新
- 易于测试:代码结构清晰
基本使用:
kotlin
// 1. 添加依赖
// implementation 'com.airbnb.android:epoxy:4.6.3'
// kapt 'com.airbnb.android:epoxy-processor:4.6.3'
// 2. 定义 Model
@EpoxyModelClass(layout = R.layout.item_user)
abstract class UserModel : EpoxyModelWithHolder<UserHolder>() {
@EpoxyAttribute
lateinit var name: String
@EpoxyAttribute
var age: Int = 0
override fun bind(holder: UserHolder) {
holder.nameTextView.text = name
holder.ageTextView.text = "$age 岁"
}
}
// 3. 创建 Holder
class UserHolder : EpoxyHolder() {
lateinit var nameTextView: TextView
lateinit var ageTextView: TextView
override fun bindView(itemView: View) {
nameTextView = itemView.findViewById(R.id.nameTextView)
ageTextView = itemView.findViewById(R.id.ageTextView)
}
}
// 4. 在 Activity 中使用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<EpoxyRecyclerView>(R.id.recyclerView)
recyclerView.withModels {
users.forEach { user ->
userModel {
id(user.id)
name(user.name)
age(user.age)
}
}
}
}
}
使用场景:
- 复杂的多类型列表
- 需要频繁更新的列表
- 需要类型安全的列表开发
12.2 如何使用 Systrace 分析性能
答案:
Systrace 是 Android 提供的性能分析工具,可以分析 RecyclerView 的性能问题。
使用步骤:
步骤 1:在代码中添加 Trace
kotlin
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 添加 Trace
Trace.beginSection("onBindViewHolder")
try {
holder.bind(items[position])
} finally {
Trace.endSection()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Trace.beginSection("onCreateViewHolder")
return try {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
ViewHolder(view)
} finally {
Trace.endSection()
}
}
}
步骤 2:使用 Systrace 工具
bash
# 1. 连接设备
adb devices
# 2. 开始录制
python systrace.py -t 10 -o trace.html sched freq idle am wm gfx view binder_driver hal dalvik camera input res
# 3. 在设备上操作 RecyclerView(滑动等)
# 4. 查看生成的 trace.html 文件
步骤 3:分析结果
在生成的 HTML 文件中,可以查看:
onBindViewHolder的执行时间onCreateViewHolder的执行时间- 主线程的阻塞情况
- 帧率情况
性能优化建议:
- 如果
onBindViewHolder耗时过长,优化数据绑定逻辑 - 如果
onCreateViewHolder耗时过长,优化布局文件 - 如果主线程阻塞,将耗时操作移到后台线程
12.3 如何使用 Layout Inspector 调试
答案:
Layout Inspector 是 Android Studio 提供的工具,可以查看 RecyclerView 的布局层次。
使用步骤:
-
打开 Layout Inspector
- Android Studio → Tools → Layout Inspector
- 或点击工具栏的 Layout Inspector 图标
-
选择设备和进程
- 选择连接的设备
- 选择要调试的应用进程
-
查看布局层次
- 左侧显示布局树
- 中间显示布局预览
- 右侧显示属性面板
-
调试 RecyclerView
- 查看 RecyclerView 的子视图
- 检查 item 的布局
- 查看 ViewHolder 的复用情况
调试技巧:
kotlin
// 在代码中添加调试信息
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// 添加标签,方便在 Layout Inspector 中识别
holder.itemView.tag = "Item_$position"
holder.bind(items[position])
}
}
十三、设计模式与最佳实践
13.1 RecyclerView 中使用的设计模式
答案:
RecyclerView 使用了多种设计模式,这是其灵活性和可扩展性的基础。
1. 适配器模式(Adapter Pattern)
kotlin
// RecyclerView.Adapter 是适配器模式的典型应用
// 将数据适配到视图
interface Adapter {
fun onCreateViewHolder(): ViewHolder
fun onBindViewHolder(holder: ViewHolder, data: Any)
}
2. 观察者模式(Observer Pattern)
kotlin
// Adapter 数据变化时,通知 RecyclerView 更新
class Adapter {
fun notifyDataSetChanged() {
// 通知所有观察者(RecyclerView)
observers.forEach { it.onChanged() }
}
}
3. 策略模式(Strategy Pattern)
kotlin
// LayoutManager 是策略模式的体现
// 不同的布局策略可以互换
interface LayoutStrategy {
fun layoutItems()
}
class LinearLayoutStrategy : LayoutStrategy { ... }
class GridLayoutStrategy : LayoutStrategy { ... }
4. 模板方法模式(Template Method Pattern)
kotlin
// RecyclerView 的绘制流程是模板方法
abstract class RecyclerView {
fun onDraw() {
drawBackground() // 固定步骤
drawDecoration() // 固定步骤
drawItems() // 可自定义
drawDecorationOver() // 固定步骤
}
}
5. 工厂模式(Factory Pattern)
kotlin
// ViewHolder 的创建使用工厂模式
class Adapter {
fun createViewHolder(type: Int): ViewHolder {
return when (type) {
TYPE_A -> ViewHolderA()
TYPE_B -> ViewHolderB()
else -> DefaultViewHolder()
}
}
}
6. 对象池模式(Object Pool Pattern)
kotlin
// RecycledViewPool 是对象池模式
class RecycledViewPool {
private val pool = mutableListOf<ViewHolder>()
fun get(): ViewHolder? = pool.removeLastOrNull()
fun put(holder: ViewHolder) = pool.add(holder)
}
13.2 如何保持 Adapter 的单一职责
答案:
单一职责原则要求一个类只负责一个功能。在 Adapter 中,应该将数据绑定和业务逻辑分离。
错误示例:
kotlin
// ❌ 错误:Adapter 承担了太多职责
class BadAdapter(private val items: List<Item>) :
RecyclerView.Adapter<BadAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
// 职责 1:数据绑定
holder.textView.text = item.text
// 职责 2:网络请求(不应该在这里)
loadImage(item.imageUrl) { bitmap ->
holder.imageView.setImageBitmap(bitmap)
}
// 职责 3:业务逻辑(不应该在这里)
if (item.isVIP) {
holder.vipBadge.visibility = View.VISIBLE
holder.textView.setTextColor(Color.GOLD)
}
// 职责 4:点击事件处理(可以,但最好分离)
holder.itemView.setOnClickListener {
// 复杂的业务逻辑
if (user.isLoggedIn) {
navigateToDetail(item)
} else {
showLoginDialog()
}
}
}
}
正确示例:
kotlin
// ✅ 正确:职责分离
class GoodAdapter(
private val items: List<Item>,
private val imageLoader: ImageLoader, // 图片加载职责分离
private val itemBinder: ItemBinder, // 数据绑定职责分离
private val clickHandler: ClickHandler // 点击处理职责分离
) : RecyclerView.Adapter<GoodAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
// 只负责协调,不处理具体逻辑
itemBinder.bind(holder, item)
imageLoader.load(item.imageUrl, holder.imageView)
holder.itemView.setOnClickListener { clickHandler.onItemClick(item) }
}
}
// 图片加载器(单一职责)
class ImageLoader {
fun load(url: String, imageView: ImageView) {
Glide.with(imageView.context)
.load(url)
.into(imageView)
}
}
// 数据绑定器(单一职责)
class ItemBinder {
fun bind(holder: ViewHolder, item: Item) {
holder.textView.text = item.text
// 业务逻辑处理
if (item.isVIP) {
holder.vipBadge.visibility = View.VISIBLE
holder.textView.setTextColor(Color.GOLD)
}
}
}
// 点击处理器(单一职责)
class ClickHandler(
private val navigator: Navigator,
private val authManager: AuthManager
) {
fun onItemClick(item: Item) {
if (authManager.isLoggedIn()) {
navigator.navigateToDetail(item)
} else {
navigator.showLoginDialog()
}
}
}
十四、补充知识点
14.1 RecyclerView 与 Jetpack Compose LazyColumn 的区别
答案:
RecyclerView 和 Compose LazyColumn 都是用于显示列表的组件,但属于不同的技术栈。
| 对比项 | RecyclerView | Compose LazyColumn |
|---|---|---|
| 技术栈 | 传统 View 系统 | Jetpack Compose |
| 声明式 | 命令式(需要 Adapter) | 声明式(直接描述 UI) |
| 代码量 | 需要 Adapter、ViewHolder | 代码更简洁 |
| 性能 | 成熟,性能优秀 | 性能优秀,但较新 |
| 学习曲线 | 相对平缓 | 需要学习 Compose |
| 兼容性 | 支持所有 Android 版本 | 需要 API 21+ |
代码对比:
kotlin
// RecyclerView 方式
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter(dataList)
}
}
// Compose LazyColumn 方式
@Composable
fun MyList(items: List<Item>) {
LazyColumn {
items(items) { item ->
ItemView(item = item)
}
}
}
选择建议:
- 使用 RecyclerView:现有项目、需要兼容低版本、团队熟悉 View 系统
- 使用 Compose LazyColumn:新项目、追求现代化、愿意学习 Compose
14.2 RecyclerView 的主要优势
答案:
RecyclerView 相比 ListView 和其他列表组件,具有以下核心优势(与 ListView 的详细对比见 1.2):
1. 灵活的布局管理
通过 LayoutManager 可以轻松切换不同的布局方式,无需修改 Adapter 代码。
kotlin
// 可以轻松切换不同的布局
recyclerView.layoutManager = LinearLayoutManager(this) // 线性
recyclerView.layoutManager = GridLayoutManager(this, 3) // 网格
recyclerView.layoutManager = StaggeredGridLayoutManager(2, VERTICAL) // 瀑布流
2. 强制 ViewHolder 模式
RecyclerView 强制使用 ViewHolder,确保性能优化(详见 2.1、2.2)。
3. 内置动画支持
默认支持增删改动画,无需手动实现(详见 8.1、8.2)。
4. 多级缓存机制
四级缓存机制提供更好的性能(详见 5.1-5.5)。
5. 高度可定制
可以自定义 LayoutManager、ItemDecoration、ItemAnimator,实现复杂的布局和效果。
6. 更好的性能
- 视图复用机制更完善(详见 11.2)
- 支持预加载(详见 9.2、14.10)
- 支持固定大小优化(详见 6.2)
总结:
RecyclerView 相比 ListView 的主要优势在于更灵活的布局管理、更好的性能、更强的可定制性。详细对比见 1.2 RecyclerView 与 ListView 的区别是什么?
14.3 ViewHolder 的生命周期
答案:
ViewHolder 的生命周期与 RecyclerView 的视图复用机制密切相关。
生命周期阶段:
markdown
1. 创建 (onCreateViewHolder)
↓
2. 绑定 (onBindViewHolder)
↓
3. 显示 (onScreen)
↓
4. 回收 (onViewRecycled)
↓
5. 复用 (onBindViewHolder) - 循环
↓
6. 销毁 (最终)
详细说明:
1. 创建阶段
kotlin
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// ViewHolder 被创建,但还未绑定数据
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ViewHolder(view)
}
2. 绑定阶段
kotlin
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// ViewHolder 绑定数据,准备显示
holder.bind(items[position])
}
3. 显示阶段
- ViewHolder 的 itemView 显示在屏幕上
- 用户可以与之交互
4. 回收阶段
kotlin
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
// ViewHolder 被回收,可以在这里清理资源
holder.clear()
}
5. 复用阶段
- ViewHolder 从缓存中取出
- 重新调用
onBindViewHolder绑定新数据
完整示例:
kotlin
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d("ViewHolder", "创建 ViewHolder")
return ViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Log.d("ViewHolder", "绑定 ViewHolder, position: $position")
holder.bind(items[position])
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
Log.d("ViewHolder", "回收 ViewHolder")
holder.clear()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: Item) {
// 绑定数据
}
fun clear() {
// 清理资源,如取消图片加载
}
}
}
14.4 notifyItemInserted() 和 notifyItemRemoved() 的区别
答案:
这两个方法用于通知 RecyclerView 数据的变化,触发相应的动画。
notifyItemInserted() - 插入通知
kotlin
// 在指定位置插入一个 item
fun addItem(position: Int, item: String) {
items.add(position, item)
notifyItemInserted(position) // 通知插入,显示插入动画
}
notifyItemRemoved() - 删除通知
kotlin
// 删除指定位置的 item
fun removeItem(position: Int) {
items.removeAt(position)
notifyItemRemoved(position) // 通知删除,显示删除动画
}
区别对比:
| 对比项 | notifyItemInserted | notifyItemRemoved |
|---|---|---|
| 作用 | 通知插入新 item | 通知删除 item |
| 动画 | 显示插入动画 | 显示删除动画 |
| 参数 | 插入的位置 | 删除的位置 |
| 使用场景 | 添加数据 | 删除数据 |
完整示例:
kotlin
class MyAdapter(private val items: MutableList<String>) :
RecyclerView.Adapter<MyAdapter.ViewHolder>() {
fun addItem(position: Int, item: String) {
items.add(position, item)
notifyItemInserted(position)
// 如果插入的不是最后一个,还需要通知后面的 item 位置变化
notifyItemRangeChanged(position + 1, items.size - position - 1)
}
fun removeItem(position: Int) {
items.removeAt(position)
notifyItemRemoved(position)
// 通知后面的 item 位置变化
notifyItemRangeChanged(position, items.size - position)
}
fun moveItem(fromPosition: Int, toPosition: Int) {
Collections.swap(items, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
}
}
最佳实践:
kotlin
// ✅ 正确:使用局部更新方法
adapter.addItem(0, "new item") // 只更新插入的 item
adapter.removeItem(5) // 只更新删除的 item
// ❌ 错误:使用全局更新
adapter.addItem(0, "new item")
adapter.notifyDataSetChanged() // 会刷新所有 item,性能差
14.5 如何实现局部刷新
答案:
局部刷新是指只更新变化的部分,而不是刷新整个列表。有多种实现方式,可以根据场景选择。
方式 1:使用 notifyItemChanged() - 单个 item 更新
kotlin
class MyAdapter(private val items: MutableList<Item>) :
RecyclerView.Adapter<MyAdapter.ViewHolder>() {
fun updateItem(position: Int, newItem: Item) {
items[position] = newItem
notifyItemChanged(position) // 只刷新这个位置的 item
}
fun updateItems(positions: List<Int>, newItems: List<Item>) {
positions.forEachIndexed { index, position ->
items[position] = newItems[index]
}
// 批量更新
positions.forEach { notifyItemChanged(it) }
}
}
方式 2:使用 DiffUtil(推荐,详见 6.3)
DiffUtil 是最智能的局部刷新方式,可以自动计算差异并更新。详细使用方法见 6.3 什么是 DiffUtil?如何使用?
适用场景:
- 需要更新整个列表时
- 数据变化复杂,难以手动计算变化时
- 需要自动显示增删改动画时
方式 3:使用 Payload 进行部分更新
kotlin
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
// 带 Payload 的绑定方法
override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isEmpty()) {
// 没有 Payload,正常绑定
super.onBindViewHolder(holder, position, payloads)
} else {
// 有 Payload,只更新变化的部分
when (payloads[0]) {
"name" -> holder.updateName(items[position].name)
"avatar" -> holder.updateAvatar(items[position].avatar)
else -> super.onBindViewHolder(holder, position, payloads)
}
}
}
fun updateItemName(position: Int, newName: String) {
items[position].name = newName
notifyItemChanged(position, "name") // 传递 Payload
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun updateName(name: String) {
// 只更新名字,不重新绑定整个 item
nameTextView.text = name
}
}
}
14.6 如何实现子 View 的点击事件
答案:
在 RecyclerView 中,除了整个 item 的点击事件,还可以为 item 内的子 View 设置独立的点击事件。
代码示例:
kotlin
class MyAdapter(
private val items: List<Item>,
private val onItemClick: (Item) -> Unit,
private val onButtonClick: (Item) -> Unit,
private val onImageClick: (Item) -> Unit
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
// Item 整体点击
holder.itemView.setOnClickListener {
onItemClick(item)
}
// 按钮点击(子 View)
holder.button.setOnClickListener {
onButtonClick(item)
}
// 图片点击(子 View)
holder.imageView.setOnClickListener {
onImageClick(item)
}
}
override fun getItemCount() = items.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val button: Button = itemView.findViewById(R.id.button)
val imageView: ImageView = itemView.findViewById(R.id.imageView)
fun bind(item: Item) {
// 绑定数据
}
}
}
注意事项:
- 子 View 的点击事件会优先于 item 的点击事件
- 如果子 View 消费了点击事件,item 的点击事件不会触发
- 可以使用
setOnClickListener和setOnLongClickListener为不同子 View 设置不同的事件
14.7 如何避免图片加载导致的滑动卡顿
答案:
图片加载是导致 RecyclerView 滑动卡顿的常见原因,需要优化加载策略。
优化方法:
1. 使用图片加载库(推荐)
kotlin
// 使用 Glide,自动处理缓存和异步加载
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.placeholder) // 占位图
.error(R.drawable.error) // 错误图
.into(imageView)
2. 在滑动时暂停加载
kotlin
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
// 只在静止时加载高清图
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
Glide.with(context)
.load(item.highResUrl)
.into(holder.imageView)
} else {
// 滑动时加载缩略图
Glide.with(context)
.load(item.thumbUrl)
.into(holder.imageView)
}
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
// 回收时取消图片加载
Glide.with(context).clear(holder.imageView)
}
}
3. 使用 RecyclerView 的滑动监听
kotlin
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
// 静止时恢复图片加载
Glide.with(context).resumeRequests()
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
// 滑动时暂停图片加载
Glide.with(context).pauseRequests()
}
}
}
})
4. 优化图片尺寸
kotlin
// 加载合适尺寸的图片,不要加载过大的图片
Glide.with(context)
.load(imageUrl)
.override(200, 200) // 限制图片尺寸
.into(imageView)
14.8 initialPrefetchItemCount 的作用
答案:
initialPrefetchItemCount 是 LayoutManager 的一个属性,用于设置 ViewHolder 的预加载数量。它可以在用户滑动之前提前创建 ViewHolder,从而提升滑动流畅度。
作用:
- 提升流畅度:提前创建 ViewHolder,减少滑动时的创建时间
- 优化体验:用户滑动时更流畅,减少卡顿
- 减少延迟:避免在滑动过程中等待 ViewHolder 创建
使用示例:
kotlin
val layoutManager = LinearLayoutManager(this)
layoutManager.initialPrefetchItemCount = 4 // 预加载 4 个 item
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
工作原理:
当前屏幕显示 item 0-9
预加载 item 10-13(提前创建 ViewHolder)
用户向下滑动时,item 10-13 已经准备好,直接显示
与数据预加载的区别:
- initialPrefetchItemCount:预加载 ViewHolder(视图层),在 RecyclerView 内部自动完成
- 数据预加载:预加载数据(数据层),需要手动监听滚动并加载更多数据(见 9.2)
注意事项:
- 预加载数量不宜过大,会占用内存
- 建议设置为 2-5 个
- 对于复杂布局,可以适当增加
- 对于简单布局,可以设置为 0 以节省内存
14.9 RecyclerView 滑动卡顿的原因和解决方案
答案:
滑动卡顿是 RecyclerView 性能问题的常见表现。
常见原因:
1. 布局嵌套过深
kotlin
// ❌ 错误:嵌套过深
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
// ✅ 正确:使用 ConstraintLayout 减少嵌套
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView />
</androidx.constraintlayout.widget.ConstraintLayout>
2. 在 onBindViewHolder 中执行耗时操作
kotlin
// ❌ 错误:在绑定中做耗时操作
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val result = complexCalculation() // 耗时操作
holder.textView.text = result
}
// ✅ 正确:提前计算好数据
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = items[position].preCalculatedResult
}
3. 图片加载未优化
kotlin
// ✅ 使用图片加载库,自动优化
Glide.with(context)
.load(imageUrl)
.into(imageView)
4. 未使用 ViewHolder 缓存
kotlin
// ✅ 正确:在 ViewHolder 中缓存视图引用
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
}
排查方法:
kotlin
// 1. 使用 Systrace 分析
Trace.beginSection("onBindViewHolder")
// ... 代码
Trace.endSection()
// 2. 使用 Profiler 查看性能
// Android Studio → View → Tool Windows → Profiler
// 3. 添加日志查看耗时
val startTime = System.currentTimeMillis()
// ... 代码
Log.d("Performance", "耗时: ${System.currentTimeMillis() - startTime}ms")
14.10 如何排查性能问题
答案:
排查性能问题需要系统的方法和工具。
排查步骤:
1. 使用 Android Profiler
kotlin
// Android Studio → View → Tool Windows → Profiler
// 可以查看 CPU、内存、网络使用情况
2. 使用 Systrace
bash
# 录制性能数据
python systrace.py -t 10 -o trace.html sched freq idle am wm gfx view
# 在设备上操作 RecyclerView
# 查看 trace.html 文件
3. 添加性能日志
kotlin
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val startTime = System.currentTimeMillis()
val holder = ViewHolder(...)
Log.d("Performance", "onCreateViewHolder 耗时: ${System.currentTimeMillis() - startTime}ms")
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val startTime = System.currentTimeMillis()
holder.bind(items[position])
Log.d("Performance", "onBindViewHolder 耗时: ${System.currentTimeMillis() - startTime}ms")
}
}
4. 检查布局层次
kotlin
// 使用 Layout Inspector 查看布局层次
// Android Studio → Tools → Layout Inspector
5. 检查内存使用
kotlin
// 检查是否有内存泄漏
// 使用 LeakCanary
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
14.11 NO_POSITION 的含义
答案:
NO_POSITION 是 RecyclerView 中的一个常量,值为 -1,表示 ViewHolder 当前没有有效的位置。
使用场景:
kotlin
// 当 ViewHolder 已经与 Adapter 分离时,adapterPosition 返回 NO_POSITION
val position = holder.adapterPosition
if (position != RecyclerView.NO_POSITION) {
// 安全访问数据
val item = items[position]
} else {
// ViewHolder 已经分离,不能访问数据
}
常见情况:
- ViewHolder 被回收:ViewHolder 被放入缓存池时
- 数据更新中:在数据更新过程中,ViewHolder 可能暂时没有位置
- 动画进行中:在动画过程中,位置可能暂时无效
最佳实践:
kotlin
// ✅ 正确:总是检查 NO_POSITION
holder.itemView.setOnClickListener {
val position = holder.adapterPosition
if (position != RecyclerView.NO_POSITION) {
val item = items[position]
onItemClick(item)
}
}
// ❌ 错误:直接使用 position 参数
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.setOnClickListener {
val item = items[position] // position 参数可能已经过时
}
}
14.12 如何实现数据源的线程安全
答案:
在多线程环境下,需要确保数据源的线程安全。
方式 1:使用同步锁
kotlin
class ThreadSafeAdapter : RecyclerView.Adapter<ViewHolder>() {
private val items = mutableListOf<String>()
private val lock = Any()
fun addItem(item: String) {
synchronized(lock) {
items.add(item)
notifyItemInserted(items.size - 1)
}
}
fun removeItem(position: Int) {
synchronized(lock) {
if (position in 0 until items.size) {
items.removeAt(position)
notifyItemRemoved(position)
}
}
}
}
方式 2:使用主线程更新
kotlin
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
private val items = mutableListOf<String>()
fun updateFromBackground(newItems: List<String>) {
// 在后台线程准备数据
Thread {
val processedItems = processItems(newItems)
// 在主线程更新 UI
Handler(Looper.getMainLooper()).post {
items.clear()
items.addAll(processedItems)
notifyDataSetChanged()
}
}.start()
}
}
方式 3:使用协程
kotlin
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
private val items = mutableListOf<String>()
fun updateItems(newItems: List<String>) {
viewModelScope.launch(Dispatchers.Default) {
// 在后台线程处理数据
val processedItems = processItems(newItems)
// 切换到主线程更新 UI
withContext(Dispatchers.Main) {
items.clear()
items.addAll(processedItems)
notifyDataSetChanged()
}
}
}
}