RecyclerView 完全指南

4 阅读16分钟

一、基础概念

1.1 什么是 RecyclerView?

答案:

RecyclerView 是 Android 提供的一个用于高效显示大量数据集合的视图组件。它是 ListView 的升级版本,提供了更灵活、更强大的功能。

主要作用:

  1. 显示列表数据:以列表、网格或瀑布流的形式展示数据
  2. 视图复用:通过 ViewHolder 模式复用视图,提高性能
  3. 灵活布局:支持多种布局方式(线性、网格、瀑布流等)
  4. 动画支持:内置动画支持,可以轻松实现增删改动画

基本使用示例:

// 布局文件
<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 的区别

答案:

这是面试中的高频题目,需要从多个维度进行对比:

对比项RecyclerViewListView
布局管理通过 LayoutManager 支持多种布局(线性、网格、瀑布流)仅支持垂直列表布局
ViewHolder强制使用 ViewHolder 模式支持但不强制
动画支持内置 ItemAnimator,支持增删改动画需要手动实现动画
分割线通过 ItemDecoration 灵活添加通过 divider 属性简单设置
性能多级缓存机制,性能更优缓存机制相对简单
灵活性高度可定制,支持自定义 LayoutManager定制性较差
点击事件需要手动实现内置 onItemClickListener

代码对比示例:

// ListView:内置点击事件
listView.setOnItemClickListener { ... }

// RecyclerView:需要手动实现点击事件
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter(data)

为什么 RecyclerView 更好?

  1. 性能更优:多级缓存机制,滑动更流畅
  2. 更灵活:可以轻松切换不同的布局方式
  3. 更现代:Google 推荐使用,ListView 已不再更新

1.3 RecyclerView 的四大核心组件

答案:

RecyclerView 的四大核心组件是:

  1. RecyclerView:容器本身,负责显示和管理子视图
  2. Adapter:数据适配器,负责将数据绑定到视图
  3. LayoutManager:布局管理器,负责子视图的排列方式
  4. ViewHolder:视图持有者,缓存视图引用,提高性能

组件关系图:

RecyclerView (容器)
    ├── LayoutManager (决定如何排列)
    ├── Adapter (决定显示什么数据)
    │   └── ViewHolder (缓存视图引用)
    └── ItemAnimator (动画效果,可选)

代码示例:

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 的调用次数

代码示例:

// ❌ 错误:每次都调用 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 调用

// 不使用 ViewHolder:每次绑定都调用 findViewById
// 假设有 1000 个 item,每个 item 有 5 个 View
// 需要调用 findViewById 5000 次!

// 使用 ViewHolder:只在创建时调用一次
// 1000 个 item,每个 item 有 5 个 View
// 只需要调用 findViewById 5000 次(创建时),但可以复用!

2. 减少内存分配

  • 视图引用被缓存,不需要重复创建
  • 减少 GC(垃圾回收)压力

3. 提高滑动流畅度

  • findViewById 是耗时操作
  • 减少调用次数,滑动更流畅
  • 避免在滑动时频繁查找视图

性能对比示例:

// 性能测试代码
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 的步骤:

代码示例:

// 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

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    // 创建并返回 ViewHolder
}

2. onBindViewHolder() - 绑定数据到 ViewHolder

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    // 将数据绑定到 ViewHolder 的视图上
}

3. getItemCount() - 返回数据总数

override fun getItemCount(): Int {
    // 返回数据列表的大小
}

完整示例:

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 中扮演不同的角色:

对比项onCreateViewHolderonBindViewHolder
调用时机创建新的 ViewHolder 时绑定数据到 ViewHolder 时
调用频率较少(只在需要新 ViewHolder 时)频繁(每次显示 item 时)
主要作用创建视图和 ViewHolder更新视图内容
性能影响影响创建性能影响滚动性能

详细说明:

onCreateViewHolder() - 创建阶段

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() - 绑定阶段

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.textView.text = items[position]  // 每次显示 item 时调用
}

性能优化建议:

// ✅ 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(推荐)

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:长按事件

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 布局。

实现步骤:

代码示例:

// 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() 是最简单的数据更新方法,但存在明显的性能问题。

优点:

  1. 使用简单:一行代码即可刷新整个列表
  2. 无需计算:不需要知道具体哪些数据变化了

缺点:

  1. 性能差:会刷新所有可见的 item,即使数据没有变化
  2. 丢失动画:无法显示增删改的动画效果
  3. 用户体验差:可能导致闪烁、滚动位置丢失等问题

代码示例:

// ❌ 不推荐:刷新所有 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

最佳实践:

// ✅ 使用 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 如何排列和显示。

主要职责:

  1. 测量子视图:计算每个 item 的大小
  2. 布局子视图:决定每个 item 的位置
  3. 回收视图:管理视图的回收和复用

代码示例:

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 - 线性布局

// 垂直列表(默认)
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 - 网格布局

// 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 - 瀑布流布局

// 2 列垂直瀑布流
val staggeredLayoutManager = StaggeredGridLayoutManager(
    2, // 列数
    StaggeredGridLayoutManager.VERTICAL // 方向
)
recyclerView.layoutManager = staggeredLayoutManager

// 3 行水平瀑布流
val horizontalStaggered = StaggeredGridLayoutManager(
    3, // 行数
    StaggeredGridLayoutManager.HORIZONTAL // 方向
)
recyclerView.layoutManager = horizontalStaggered

布局效果对比:

LinearLayoutManager (垂直):
┌─────┐
│  1  │
├─────┤
│  2  │
├─────┤
│  3  │
└─────┘

GridLayoutManager (2列):
┌─────┬─────┐
│  12  │
├─────┼─────┤
│  34  │
└─────┴─────┘

StaggeredGridLayoutManager (2列):
┌─────┬─────┐
│  12  │
│     ├─────┤
│     │  3  │
├─────┤     │
│  4  │     │
└─────┴─────┘

五、缓存机制

5.1 RecyclerView 的缓存机制

答案:

RecyclerView 采用四级缓存机制,这是其高性能的核心。

四级缓存结构:

RecyclerView 缓存机制
├── 一级缓存:mAttachedScrap(屏幕内缓存)
├── 二级缓存:mCachedViews(屏幕外缓存)
├── 三级缓存:ViewCacheExtension(自定义缓存,可选)
└── 四级缓存:RecycledViewPool(回收池)

详细说明:

1. 一级缓存(mAttachedScrap)

  • 作用:存储当前屏幕内可见的 ViewHolder
  • 特点:数据未变化时直接复用,无需重新绑定
  • 场景:数据局部更新时使用
// 当调用 notifyItemChanged(5) 时
// 第 5 个 item 会先放入 mAttachedScrap
// 然后重新绑定数据,放回原位置

2. 二级缓存(mCachedViews)

  • 作用:存储刚滑出屏幕的 ViewHolder
  • 特点:默认最多缓存 2 个,数据未变化
  • 场景:快速来回滑动时复用
// 用户向下滑动,item 1 滑出屏幕
// item 1 的 ViewHolder 放入 mCachedViews
// 如果用户立即向上滑动,可以直接复用

3. 三级缓存(ViewCacheExtension)

  • 作用:开发者自定义的缓存层
  • 特点:可选,大多数情况下不需要
  • 场景:特殊缓存需求

4. 四级缓存(RecycledViewPool)

  • 作用:存储所有类型的 ViewHolder
  • 特点:数据已清空,需要重新绑定
  • 场景:跨 RecyclerView 共享,或作为最后备选

缓存查找顺序:

// 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。

主要作用:

  1. 局部更新优化:当调用 notifyItemChanged() 时,避免重新创建 ViewHolder
  2. 快速复用:数据未变化时直接复用,无需重新绑定
  3. 保持状态:保持 ViewHolder 的选中状态、动画状态等

工作原理:

更新 item 5:
1. ViewHolder 放入 mAttachedScrap
2. 调用 onBindViewHolder 重新绑定
3. ViewHolder 取出复用
结果:ViewHolder 复用,只更新数据

代码示例:

fun updateItem(position: Int, newText: String) {
    items[position] = newText
    notifyItemChanged(position)  // ViewHolder 复用,只更新数据
}

优势:

  • 性能好:不需要重新创建 View
  • 保持状态:保持用户交互状态(如选中、展开等)
  • 流畅:更新过程更平滑

5.3 二级缓存(mCachedViews)的作用

答案:

mCachedViews 存储刚滑出屏幕的 ViewHolder,用于快速来回滑动时的复用。

主要特点:

  1. 默认容量:最多缓存 2 个 ViewHolder
  2. 数据完整:ViewHolder 中的数据未清空
  3. 快速复用:可以直接使用,无需重新绑定

工作原理:

向下滑动:item 0 滑出 → 放入 mCachedViews(数据保留)
向上滑动:item 0 从 mCachedViews 直接复用(无需重新绑定)
继续滑动:mCachedViews 满(2个)→ 移入 RecycledViewPool

为什么只缓存 2 个?

  • 平衡内存和性能
  • 大多数情况下,用户来回滑动不会超过 2 个 item
  • 如果缓存太多,会占用过多内存

5.4 三级缓存(ViewCacheExtension)的作用

答案:

ViewCacheExtension 是 RecyclerView 的第三级缓存,是开发者可以自定义的缓存层。

主要特点:

  1. 可选缓存:大多数情况下不需要使用
  2. 自定义实现:开发者可以自定义缓存逻辑
  3. 特殊场景:适用于有特殊缓存需求的场景

使用场景:

  • 需要特殊的缓存策略
  • 需要跨 RecyclerView 共享特定类型的 ViewHolder
  • 需要自定义缓存的生命周期

代码示例:

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。

主要特点:

  1. 数据已清空:ViewHolder 中的数据已被清空
  2. 需要重新绑定:使用时需要调用 onBindViewHolder
  3. 可共享:多个 RecyclerView 可以共享同一个 Pool
  4. 按类型存储:不同类型的 ViewHolder 分开存储

工作原理:

// ViewHolder 进入 RecycledViewPool 的流程:

// 1. ViewHolder 从 mCachedViews 移出
// 2. 清空 ViewHolder 中的数据(调用 onViewRecycled)
// 3. 放入 RecycledViewPool

// 使用时:
// 1. 从 RecycledViewPool 获取 ViewHolder
// 2. 调用 onBindViewHolder 重新绑定数据
// 3. 显示在屏幕上

代码示例:共享 RecycledViewPool

val sharedPool = RecyclerView.RecycledViewPool()
sharedPool.setMaxRecycledViews(0, 20)

recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)  // 共享缓存

优势:

  • 内存优化:多个 RecyclerView 共享缓存,减少内存占用
  • 性能提升:避免重复创建 ViewHolder
  • 灵活性:可以自定义缓存数量

六、性能优化

6.1 如何优化性能

答案:

RecyclerView 性能优化是一个综合性的工作,需要从多个方面入手。

优化策略:

// 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,它的尺寸是固定的,不会因为内容变化而改变大小。

作用:

  1. 优化布局计算:RecyclerView 知道大小不变,可以跳过一些布局测量
  2. 提高性能:减少不必要的布局重新计算

使用场景:

// ✅ 适合使用: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) // 大小可能改变

代码示例:

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 提供的一个工具类,用于计算两个列表之间的差异,并生成更新操作。

主要优势:

  1. 增量更新:只更新变化的部分,而不是整个列表
  2. 自动动画:配合 RecyclerView 可以自动显示增删改动画
  3. 性能优化:避免不必要的视图刷新

使用方法:

代码示例:

// 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 之间添加装饰效果,如分割线、间距、背景等。

主要作用:

  1. 添加分割线:在 item 之间绘制分割线
  2. 设置间距:为 item 添加内边距或外边距
  3. 绘制背景:为 item 添加背景装饰
  4. 自定义装饰:实现复杂的装饰效果

基本使用:

// 添加分割线
val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)

7.2 如何自定义 ItemDecoration

答案:

自定义 ItemDecoration 需要继承 RecyclerView.ItemDecoration 并重写相应方法。

核心方法:

  1. getItemOffsets() - 设置 item 的偏移量(为装饰留出空间)
  2. onDraw() - 在 item 下方绘制装饰
  3. onDrawOver() - 在 item 上方绘制装饰(详见 7.3)

代码示例:自定义分割线

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 上方绘制
绘制顺序先绘制后绘制
使用场景分割线、背景悬浮效果、遮罩

绘制顺序:

绘制顺序(从下到上):
1. RecyclerView 背景
2. onDraw() 绘制的内容(分割线等)
3. Item 视图
4. onDrawOver() 绘制的内容(悬浮效果等)

代码示例:

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 的增删改动画效果。

主要作用:

  1. 添加动画:item 添加时的动画
  2. 删除动画:item 删除时的动画
  3. 移动动画:item 位置变化时的动画
  4. 更改动画:item 内容变化时的动画

默认动画:

// RecyclerView 使用 DefaultItemAnimator 作为默认动画
recyclerView.itemAnimator = DefaultItemAnimator()

8.2 如何自定义 ItemAnimator

答案:

自定义 ItemAnimator 需要继承 RecyclerView.ItemAnimator 并实现相应方法。

核心方法:

  1. animateAdd() - 处理添加动画
  2. animateRemove() - 处理删除动画
  3. animateMove() - 处理移动动画
  4. animateChange() - 处理更改动画

代码示例:简单的淡入淡出动画

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,但需要注意一些优化点。

基本使用:

// 如果 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)
    }
}

性能优化:

// 如果所有 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 如何实现数据预加载

答案:

数据预加载是指监听滚动事件,在用户滑动到列表底部之前就开始加载更多数据,从而提升用户体验,避免用户等待数据加载。

实现方式:

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

// ❌ 错误:没有设置 LayoutManager
recyclerView.adapter = adapter

// ✅ 正确:必须设置 LayoutManager
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter

2. 数据为空

// 检查数据是否为空
if (dataList.isEmpty()) {
    // 显示空状态视图
    showEmptyView()
} else {
    recyclerView.adapter = MyAdapter(dataList)
}

3. Item 布局高度为 0

<!-- ❌ 错误:高度为 0 -->
<TextView
    android:layout_height="0dp" />

<!-- ✅ 正确:设置合适的高度 -->
<TextView
    android:layout_height="wrap_content" />

4. RecyclerView 高度问题

<!-- ❌ 错误:高度为 wrap_content 且没有内容 -->
<RecyclerView
    android:layout_height="wrap_content" />

<!-- ✅ 正确:使用 match_parent 或固定高度 -->
<RecyclerView
    android:layout_height="match_parent" />

5. Adapter 的 getItemCount() 返回 0

// 检查 getItemCount() 是否正确
override fun getItemCount(): Int {
    return items.size // 确保返回正确的数量
}

调试方法:

// 添加日志调试
Log.d("RecyclerView", "ItemCount: ${adapter.itemCount}")
Log.d("RecyclerView", "DataSize: ${dataList.size}")
Log.d("RecyclerView", "LayoutManager: ${recyclerView.layoutManager}")

10.2 如何避免数据更新异常

答案:

数据更新时的异常通常是由于在错误的时机更新数据导致的。

常见异常及解决方案:

1. IndexOutOfBoundsException

// ❌ 错误:在后台线程更新数据后直接通知
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. 并发修改异常

// ❌ 错误:在遍历时修改列表
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. 位置不匹配异常

// ✅ 正确:使用 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] // 安全访问
        }
    }
}

最佳实践:

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

// ❌ 错误: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. 未取消异步任务

// ❌ 错误:异步任务未取消
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. 未移除监听器

// ✅ 正确:在 onViewRecycled 中移除监听器
override fun onViewRecycled(holder: ViewHolder) {
    super.onViewRecycled(holder)
    holder.removeListeners()
}

十一、源码分析

11.1 RecyclerView 的绘制流程

答案:

RecyclerView 的绘制流程遵循 Android 的标准绘制流程,但加入了视图复用机制。

绘制流程:

1. onMeasure() - 测量阶段
   ├── 测量 RecyclerView 自身大小
   ├── 测量可见的 item
   └── 计算总高度/宽度

2. onLayout() - 布局阶段
   ├── 调用 LayoutManager.onLayoutChildren()
   ├── 回收不可见的 ViewHolder
   ├── 复用或创建新的 ViewHolder
   └── 布局可见的 item

3. onDraw() - 绘制阶段
   ├── 绘制 RecyclerView 背景
   ├── 绘制 ItemDecoration (onDraw)
   ├── 绘制 item 视图
   └── 绘制 ItemDecoration (onDrawOver)

关键源码分析:

// 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)
}

视图复用流程:

// 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)

// 当 item 滑出屏幕时
fun recycleView(view: View) {
    val holder = getChildViewHolder(view)
    
    // 1. 清除数据绑定
    holder.unbind()
    
    // 2. 根据情况放入缓存
    if (holder.isRecyclable) {
        // 放入 RecycledViewPool
        recycledViewPool.putRecycledView(holder)
    }
}

2. 视图获取(Get)

// 需要 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. 缓存策略

// 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()
    }
}

复用流程图:

用户滑动列表
    ↓
Item 滑出屏幕
    ↓
ViewHolder 被回收
    ↓
放入 RecycledViewPool
    ↓
新的 Item 需要显示
    ↓
从 RecycledViewPool 获取 ViewHolder
    ↓
重新绑定数据 (onBindViewHolder)
    ↓
显示在屏幕上

11.3 LayoutManager 的测量和布局流程

答案:

LayoutManager 负责测量和布局 RecyclerView 中的 item。

测量流程(Measure):

// 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):

// 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(): 将视图添加到 RecyclerView
  • measureChildWithMargins(): 测量子视图(包含 margin)
  • layoutDecorated(): 布局子视图(包含 decoration 的偏移)
  • recycleViewsOutOfBounds(): 回收超出边界的视图

十二、第三方库与工具

12.1 Epoxy 库的作用和使用场景

答案:

Epoxy 是 Airbnb 开发的一个库,用于简化 RecyclerView 的 Adapter 开发。

主要优势:

  1. 简化代码:减少样板代码
  2. 类型安全:编译时检查
  3. 自动 Diff:自动计算差异并更新
  4. 易于测试:代码结构清晰

基本使用:

// 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

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 工具

# 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 的布局层次。

使用步骤:

  1. 打开 Layout Inspector

    • Android Studio → Tools → Layout Inspector
    • 或点击工具栏的 Layout Inspector 图标
  2. 选择设备和进程

    • 选择连接的设备
    • 选择要调试的应用进程
  3. 查看布局层次

    • 左侧显示布局树
    • 中间显示布局预览
    • 右侧显示属性面板
  4. 调试 RecyclerView

    • 查看 RecyclerView 的子视图
    • 检查 item 的布局
    • 查看 ViewHolder 的复用情况

调试技巧:

// 在代码中添加调试信息
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)

// RecyclerView.Adapter 是适配器模式的典型应用
// 将数据适配到视图
interface Adapter {
    fun onCreateViewHolder(): ViewHolder
    fun onBindViewHolder(holder: ViewHolder, data: Any)
}

2. 观察者模式(Observer Pattern)

// Adapter 数据变化时,通知 RecyclerView 更新
class Adapter {
    fun notifyDataSetChanged() {
        // 通知所有观察者(RecyclerView)
        observers.forEach { it.onChanged() }
    }
}

3. 策略模式(Strategy Pattern)

// LayoutManager 是策略模式的体现
// 不同的布局策略可以互换
interface LayoutStrategy {
    fun layoutItems()
}

class LinearLayoutStrategy : LayoutStrategy { ... }
class GridLayoutStrategy : LayoutStrategy { ... }

4. 模板方法模式(Template Method Pattern)

// RecyclerView 的绘制流程是模板方法
abstract class RecyclerView {
    fun onDraw() {
        drawBackground()      // 固定步骤
        drawDecoration()       // 固定步骤
        drawItems()            // 可自定义
        drawDecorationOver()   // 固定步骤
    }
}

5. 工厂模式(Factory Pattern)

// ViewHolder 的创建使用工厂模式
class Adapter {
    fun createViewHolder(type: Int): ViewHolder {
        return when (type) {
            TYPE_A -> ViewHolderA()
            TYPE_B -> ViewHolderB()
            else -> DefaultViewHolder()
        }
    }
}

6. 对象池模式(Object Pool Pattern)

// RecycledViewPool 是对象池模式
class RecycledViewPool {
    private val pool = mutableListOf<ViewHolder>()
    
    fun get(): ViewHolder? = pool.removeLastOrNull()
    fun put(holder: ViewHolder) = pool.add(holder)
}

13.2 如何保持 Adapter 的单一职责

答案:

单一职责原则要求一个类只负责一个功能。在 Adapter 中,应该将数据绑定和业务逻辑分离。

错误示例:

// ❌ 错误: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()
            }
        }
    }
}

正确示例:

// ✅ 正确:职责分离
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 都是用于显示列表的组件,但属于不同的技术栈。

对比项RecyclerViewCompose LazyColumn
技术栈传统 View 系统Jetpack Compose
声明式命令式(需要 Adapter)声明式(直接描述 UI)
代码量需要 Adapter、ViewHolder代码更简洁
性能成熟,性能优秀性能优秀,但较新
学习曲线相对平缓需要学习 Compose
兼容性支持所有 Android 版本需要 API 21+

代码对比:

// 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 代码。

// 可以轻松切换不同的布局
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 的视图复用机制密切相关。

生命周期阶段:

1. 创建 (onCreateViewHolder)
   ↓
2. 绑定 (onBindViewHolder)
   ↓
3. 显示 (onScreen)
   ↓
4. 回收 (onViewRecycled)
   ↓
5. 复用 (onBindViewHolder) - 循环
   ↓
6. 销毁 (最终)

详细说明:

1. 创建阶段

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. 绑定阶段

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    // ViewHolder 绑定数据,准备显示
    holder.bind(items[position])
}

3. 显示阶段

  • ViewHolder 的 itemView 显示在屏幕上
  • 用户可以与之交互

4. 回收阶段

override fun onViewRecycled(holder: ViewHolder) {
    super.onViewRecycled(holder)
    // ViewHolder 被回收,可以在这里清理资源
    holder.clear()
}

5. 复用阶段

  • ViewHolder 从缓存中取出
  • 重新调用 onBindViewHolder 绑定新数据

完整示例:

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() - 插入通知

// 在指定位置插入一个 item
fun addItem(position: Int, item: String) {
    items.add(position, item)
    notifyItemInserted(position) // 通知插入,显示插入动画
}

notifyItemRemoved() - 删除通知

// 删除指定位置的 item
fun removeItem(position: Int) {
    items.removeAt(position)
    notifyItemRemoved(position) // 通知删除,显示删除动画
}

区别对比:

对比项notifyItemInsertednotifyItemRemoved
作用通知插入新 item通知删除 item
动画显示插入动画显示删除动画
参数插入的位置删除的位置
使用场景添加数据删除数据

完整示例:

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)
    }
}

最佳实践:

// ✅ 正确:使用局部更新方法
adapter.addItem(0, "new item") // 只更新插入的 item
adapter.removeItem(5) // 只更新删除的 item

// ❌ 错误:使用全局更新
adapter.addItem(0, "new item")
adapter.notifyDataSetChanged() // 会刷新所有 item,性能差

14.5 如何实现局部刷新

答案:

局部刷新是指只更新变化的部分,而不是刷新整个列表。有多种实现方式,可以根据场景选择。

方式 1:使用 notifyItemChanged() - 单个 item 更新

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 进行部分更新

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 设置独立的点击事件。

代码示例:

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 的点击事件不会触发
  • 可以使用 setOnClickListenersetOnLongClickListener 为不同子 View 设置不同的事件

14.7 如何避免图片加载导致的滑动卡顿

答案:

图片加载是导致 RecyclerView 滑动卡顿的常见原因,需要优化加载策略。

优化方法:

1. 使用图片加载库(推荐)

// 使用 Glide,自动处理缓存和异步加载
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder) // 占位图
    .error(R.drawable.error) // 错误图
    .into(imageView)

2. 在滑动时暂停加载

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 的滑动监听

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. 优化图片尺寸

// 加载合适尺寸的图片,不要加载过大的图片
Glide.with(context)
    .load(imageUrl)
    .override(200, 200) // 限制图片尺寸
    .into(imageView)

14.8 initialPrefetchItemCount 的作用

答案:

initialPrefetchItemCount 是 LayoutManager 的一个属性,用于设置 ViewHolder 的预加载数量。它可以在用户滑动之前提前创建 ViewHolder,从而提升滑动流畅度。

作用:

  • 提升流畅度:提前创建 ViewHolder,减少滑动时的创建时间
  • 优化体验:用户滑动时更流畅,减少卡顿
  • 减少延迟:避免在滑动过程中等待 ViewHolder 创建

使用示例:

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. 布局嵌套过深

// ❌ 错误:嵌套过深
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

// ✅ 正确:使用 ConstraintLayout 减少嵌套
<androidx.constraintlayout.widget.ConstraintLayout>
    <TextView />
</androidx.constraintlayout.widget.ConstraintLayout>

2. 在 onBindViewHolder 中执行耗时操作

// ❌ 错误:在绑定中做耗时操作
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. 图片加载未优化

// ✅ 使用图片加载库,自动优化
Glide.with(context)
    .load(imageUrl)
    .into(imageView)

4. 未使用 ViewHolder 缓存

// ✅ 正确:在 ViewHolder 中缓存视图引用
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val textView: TextView = itemView.findViewById(R.id.textView)
}

排查方法:

// 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

// Android Studio → View → Tool Windows → Profiler
// 可以查看 CPU、内存、网络使用情况

2. 使用 Systrace

# 录制性能数据
python systrace.py -t 10 -o trace.html sched freq idle am wm gfx view

# 在设备上操作 RecyclerView

# 查看 trace.html 文件

3. 添加性能日志

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. 检查布局层次

// 使用 Layout Inspector 查看布局层次
// Android Studio → Tools → Layout Inspector

5. 检查内存使用

// 检查是否有内存泄漏
// 使用 LeakCanary
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}

14.11 NO_POSITION 的含义

答案:

NO_POSITION 是 RecyclerView 中的一个常量,值为 -1,表示 ViewHolder 当前没有有效的位置。

使用场景:

// 当 ViewHolder 已经与 Adapter 分离时,adapterPosition 返回 NO_POSITION
val position = holder.adapterPosition
if (position != RecyclerView.NO_POSITION) {
    // 安全访问数据
    val item = items[position]
} else {
    // ViewHolder 已经分离,不能访问数据
}

常见情况:

  1. ViewHolder 被回收:ViewHolder 被放入缓存池时
  2. 数据更新中:在数据更新过程中,ViewHolder 可能暂时没有位置
  3. 动画进行中:在动画过程中,位置可能暂时无效

最佳实践:

// ✅ 正确:总是检查 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:使用同步锁

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:使用主线程更新

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:使用协程

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()
            }
        }
    }
}