RecyclerView 完全指南

一、基础概念

1.1 什么是 RecyclerView?

答案:

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

主要作用:

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

基本使用示例:

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 更好?

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

1.3 RecyclerView 的四大核心组件

答案:

RecyclerView 的四大核心组件是:

  1. RecyclerView:容器本身,负责显示和管理子视图
  2. Adapter:数据适配器,负责将数据绑定到视图
  3. LayoutManager:布局管理器,负责子视图的排列方式
  4. 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() 是最简单的数据更新方法,但存在明显的性能问题。

优点:

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

缺点:

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

代码示例:

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

主要职责:

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

代码示例:

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。

主要作用:

  1. 局部更新优化 :当调用 notifyItemChanged() 时,避免重新创建 ViewHolder
  2. 快速复用:数据未变化时直接复用,无需重新绑定
  3. 保持状态:保持 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,用于快速来回滑动时的复用。

主要特点:

  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
  • 需要自定义缓存的生命周期

代码示例:

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。

主要特点:

  1. 数据已清空:ViewHolder 中的数据已被清空
  2. 需要重新绑定 :使用时需要调用 onBindViewHolder
  3. 可共享:多个 RecyclerView 可以共享同一个 Pool
  4. 按类型存储:不同类型的 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,它的尺寸是固定的,不会因为内容变化而改变大小。

作用:

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

使用场景:

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

主要优势:

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

使用方法:

代码示例:

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

主要作用:

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

基本使用:

kotlin 复制代码
// 添加分割线
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)

代码示例:自定义分割线

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

主要作用:

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

默认动画:

kotlin 复制代码
// RecyclerView 使用 DefaultItemAnimator 作为默认动画
recyclerView.itemAnimator = DefaultItemAnimator()

8.2 如何自定义 ItemAnimator

答案:

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

核心方法:

  1. animateAdd() - 处理添加动画
  2. animateRemove() - 处理删除动画
  3. animateMove() - 处理移动动画
  4. 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(): 将视图添加到 RecyclerView
  • measureChildWithMargins(): 测量子视图(包含 margin)
  • layoutDecorated(): 布局子视图(包含 decoration 的偏移)
  • recycleViewsOutOfBounds(): 回收超出边界的视图

十二、第三方库与工具

12.1 Epoxy 库的作用和使用场景

答案:

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

主要优势:

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

基本使用:

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

使用步骤:

  1. 打开 Layout Inspector

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

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

    • 左侧显示布局树
    • 中间显示布局预览
    • 右侧显示属性面板
  4. 调试 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 的点击事件不会触发
  • 可以使用 setOnClickListenersetOnLongClickListener 为不同子 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 已经分离,不能访问数据
}

常见情况:

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

最佳实践:

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()
            }
        }
    }
}
相关推荐
青莲8432 小时前
Android WebView 混合开发完整指南
android·前端·面试
GIS之路2 小时前
GDAL 实现矢量数据转换处理(全)
前端
大厂技术总监下海3 小时前
Rust的“一发逆转弹”:Dioxus 如何用一套代码横扫 Web、桌面、移动与后端?
前端·rust·开源
加洛斯3 小时前
SpringSecurity入门篇(2):替换登录页与config配置
前端·后端
用户904706683573 小时前
Nuxt详解 —— 设置seo以及元数据
前端
DarkLONGLOVE3 小时前
Vue组件使用三步走:创建、注册、使用(Vue2/Vue3双版本详解)
前端·javascript·vue.js
DarkLONGLOVE3 小时前
手把手教你玩转Vue组件:创建、注册、使用三步曲!
前端·javascript·vue.js
龙之叶3 小时前
【Android Monkey源码解析三】- 运行解析
android
李剑一3 小时前
uni-app实现leaflet地图图标旋转
前端·trae