Android UI 组件系列(九):ListView 性能优化与 ViewHolder 模式实战

博客专栏:Android初级入门UI组件与布局

源码:通过网盘分享的文件:Android入门布局及UI相关案例

链接: https://pan.baidu.com/s/1EOuDUKJndMISolieFSvXXg?pwd=4k9n 提取码: 4k9n

引言

在上一篇文章《Android UI 组件系列(八):ListView 基础用法与适配器详解》中,我们学习了如何通过 ArrayAdapter 或 SimpleAdapter 快速构建一个 ListView 列表,并实现了简单的点击事件和图文混排。

这些内容虽然可以满足大多数"原型阶段"或"低频操作"的列表需求,但一旦涉及到:

  • 大数据量 的展示;
  • 频繁滚动 的交互;
  • 自定义复杂布局(如图标、文字、按钮并存);

就会遇到非常明显的卡顿、内存占用高的问题。这背后的关键,其实就是我们常说的 getView() 频繁创建 View 布局而未复用的问题。

🎯 所以本篇的核心目标是:

🧠 搞懂 getView() 的循环机制、掌握 ViewHolder 模式的优化技巧,并学会使用 BaseAdapter 构建高性能的复杂列表 UI。

我们还将简要对比 ListView 与 RecyclerView 在性能、灵活性方面的差异,为后续迁移做好准备。

如果你正在使用 ListView 开发中大型列表页面,又想让滑动丝滑不卡顿,这一篇你一定要看完!

一、getView() 的复用机制

在 ListView 中,getView() 是最核心的性能关键点。每一个列表项在显示时,系统都会回调一次 getView() 方法,由你来负责"返回该位置所需的 View"。

🌀 为什么会卡顿?

如果你在 getView() 中每次都执行:

  • LayoutInflater.inflate(...) 创建新 View;
  • findViewById(...) 查找子控件;

当列表有几十甚至上百项时,每次滚动都会重复执行这些开销操作 ------ 滑动顿挫、内存抖动,就是这样来的。

🧩 convertView 是什么?

getView() 方法的标准签名是:

Kotlin 复制代码
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View

其中:

  • convertView:系统传进来的 "可复用的旧 View",如果为 null,说明要新创建;不为 null,就可以复用,节省开销;
  • parent:当前列表的父容器 ListView 本身;

🚀 标准复用流程

你应该这样使用 convertView 来进行判断和复用:

Kotlin 复制代码
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val view: View = convertView ?: LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)

    val textView = view.findViewById<TextView>(R.id.text_view)
    textView.text = data[position]

    return view
}

这段代码的意思是:

  • 如果 convertView 为 null,就新建一个 View;
  • 否则复用旧 View,避免重复构建;

🧠 实际运行时是这样的

用户打开页面:

→ 系统初始化列表前几个 item → getView() 被调用 N 次(初次加载)

用户滑动列表:

→ 顶部 item 滑出屏幕 → 系统将旧 view 传入 convertView

→ getView() 使用 convertView 进行复用(无需新建)

🛠️代码实现如下

我们在textView上来标记处哪些是创建的哪些是复用的。

Kotlin 复制代码
    /// getview() 方法的自定义适配器
    private fun setupCustomAdapter() {
        val listView = findViewById<ListView>(R.id.list_view)
        val data = List(20) { index ->
            mapOf("title" to "微信 #$index", "icon" to R.drawable.ic_wechat)
        }

        val adapter = object : android.widget.BaseAdapter() {
            override fun getCount(): Int = data.size
            override fun getItem(position: Int): Any = data[position]
            override fun getItemId(position: Int): Long = position.toLong()

            override fun getView(position: Int, convertView: android.view.View?, parent: android.view.ViewGroup): android.view.View {
                val view = convertView ?: layoutInflater.inflate(R.layout.list_item, parent, false)
                val imageView = view.findViewById<android.widget.ImageView>(R.id.image_view)
                val textView = view.findViewById<android.widget.TextView>(R.id.text_view)

                val item = data[position]
                imageView.setImageResource(item["icon"] as Int)
                val title = item["title"]?.toString() ?: "未知"
                val state = if (convertView == null) " 创建" else " 复用"
                textView.text = title + state
                return view
            }
        }

        listView.adapter = adapter
    }

效果如下:

我们发现只有首次出现的屏幕的上的视图是创建的,而从屏幕外出现的所有视图都是复用的已经创建好的视图。

但我们这一步只解决了视图的重复创建问题,但每次仍然需要执行view.findViewById在视图上来查找UI组件,接下来就是ViewHolder出场的时候了。

二、ViewHolder 的作用与实现

在上一节中我们提到,虽然我们通过 convertView 复用了 item 布局本身,但每次执行 getView() 时,仍然需要重新调用 findViewById() 来查找子控件(如 TextView、ImageView),这是一个相对昂贵的操作。

🎯 问题复盘

Kotlin 复制代码
val imageView = view.findViewById<ImageView>(R.id.image_view)
val textView = view.findViewById<TextView>(R.id.text_view)

这段代码在滑动过程中会被反复调用,而其实每个 item 的控件结构是固定的,只需要找一遍即可。我们需要一种方式把这些"已经找过的控件"缓存起来。

✅ ViewHolder 是什么?

ViewHolder 本质上是一个静态内部类 ,用来缓存每个 item 布局中的子控件引用,避免每次滑动都调用 findViewById()。

它通常搭配 setTag() / getTag() 使用,在首次加载时创建 ViewHolder 并绑定,在复用时直接取出使用。

🧩 ViewHolder 的标准用法

Kotlin 复制代码
class ViewHolder(val imageView: ImageView, val textView: TextView)

然后在 getView() 中这样写:

Kotlin 复制代码
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val view: View
    val holder: ViewHolder

    if (convertView == null) {
        view = layoutInflater.inflate(R.layout.list_item, parent, false)
        val imageView = view.findViewById<ImageView>(R.id.image_view)
        val textView = view.findViewById<TextView>(R.id.text_view)
        holder = ViewHolder(imageView, textView)
        view.tag = holder
    } else {
        view = convertView
        holder = view.tag as ViewHolder
    }

    val item = data[position]
    holder.imageView.setImageResource(item["icon"] as Int)
    holder.textView.text = item["title"] as String

    return view
}

🚀 优化效果

使用 ViewHolder 后:

  • findViewById() 只执行一次;
  • 后续滑动时直接复用已有控件;
  • 滑动更流畅,卡顿概率显著降低;
  • 是开发中必须掌握的基础优化技巧。

三、使用 BaseAdapter 自定义复杂布局

我们准备展示一个"新闻卡片"列表项,包含以下字段:

  • 新闻封面图(图片)
  • 新闻标题(内容)
  • 发布时间(日期)
  • 点赞图标(根据点赞状态变化)

✅ 第一步:定义数据模型

包含标题、日期、图片,点赞状态。

Kotlin 复制代码
data class NewsItem(
    val title: String,
    val date: String,
    val imageResId: Int,
    var liked: Boolean = false
)

✅ 第二步:设计布局 news_item.xml

放在 res/layout/news_item.xml,示例结构如下:

XML 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="12dp"
    android:background="@android:color/white">

    <ImageView
        android:id="@+id/image_cover"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:scaleType="centerCrop"
        android:src="@drawable/news_placeholder" />

    <TextView
        android:id="@+id/text_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textColor="#000"
        android:textStyle="bold"
        android:paddingTop="8dp"
        android:text="新闻标题" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="space_between"
        android:paddingTop="4dp">

        <TextView
            android:id="@+id/text_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="2025-07-23"
            android:textColor="#888"
            android:textSize="14sp" />

        <ImageView
            android:id="@+id/image_like"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:src="@drawable/ic_like_off" />
    </LinearLayout>
</LinearLayout>

✅ 第三步:准备数据源

你可以在 setupNewsAdapter() 中构造 10 条 NewsItem 模拟数据:

Kotlin 复制代码
val newsList = List(10) { index ->
    NewsItem(
        title = "这是第 $index 条新闻内容",
        date = "2025-07-23",
        imageResId = R.drawable.news_placeholder,
        liked = index % 2 == 0 // 偶数默认已点赞
    )
}

**✅**第四步:实现列表

Kotlin 复制代码
/// 使用 BaseAdapter 展示新闻卡片列表
    private fun setupNewsAdapter() {
        val listView = findViewById<ListView>(R.id.list_view)

        // 模拟新闻数据
        val newsList = List(10) { index ->
            NewsItem(
                title = "这是第 $index 条新闻内容",
                date = "2025-07-23",
                imageResId = R.drawable.news_placeholder,
                liked = index % 2 == 0
            )
        }

        val adapter = object : android.widget.BaseAdapter() {


            override fun getCount(): Int = newsList.size
            override fun getItem(position: Int): Any = newsList[position]
            override fun getItemId(position: Int): Long = position.toLong()

            override fun getView(position: Int, convertView: android.view.View?, parent: android.view.ViewGroup): android.view.View {
                val view: android.view.View
                val holder: NewsViewHolder

                if (convertView == null) {
                    view = layoutInflater.inflate(R.layout.news_item, parent, false)
                    val imageView = view.findViewById<android.widget.ImageView>(R.id.image_cover)
                    val titleView = view.findViewById<android.widget.TextView>(R.id.text_title)
                    val dateView = view.findViewById<android.widget.TextView>(R.id.text_date)
                    val likeView = view.findViewById<android.widget.ImageView>(R.id.image_like)
                    holder = NewsViewHolder(imageView, titleView, dateView, likeView)
                    view.tag = holder
                } else {
                    view = convertView
                    holder = view.tag as NewsViewHolder
                }

                val item = newsList[position]
                holder.imageView.setImageResource(item.imageResId)
                holder.titleView.text = item.title
                holder.dateView.text = item.date
                holder.likeView.setImageResource(
                    if (item.liked) R.drawable.ic_like_on else R.drawable.ic_like_off
                )

                return view
            }
        }

        listView.adapter = adapter
    }

其中NewsViewHolder实现如下:

Kotlin 复制代码
private class NewsViewHolder(
        val imageView: android.widget.ImageView,
        val titleView: android.widget.TextView,
        val dateView: android.widget.TextView,
        val likeView: android.widget.ImageView
    )

最终效果如下:

📌 结语

通过本篇内容,我们围绕 ListView 的性能优化做了逐步深入:

  • ✅ 了解了 getView() 的调用机制与 convertView 的复用原理;

  • ✅ 学会了使用 ViewHolder 缓存子控件,避免重复调用 findViewById();

  • ✅ 使用 BaseAdapter 构建了一个图文混排的"新闻卡片"列表,完整演示了高性能 ListView 的实现方式。

🎯 为什么我们还要学 ListView?

虽然 RecyclerView 已成为 Android 的主流列表组件,但理解 ListView 的机制依然非常关键

  1. 很多老项目仍在使用 ListView,维护时需要具备优化能力;
  2. RecyclerView 中的 ViewHolder、回收机制,其设计理念本就源于 ListView 的优化实践;
  3. 对初学者来说,ListView 是入门列表原理、掌握 Adapter 模型的绝佳起点。

⏭️ 再进一步:迈向 RecyclerView

相比之下,RecyclerView 提供了更强的扩展能力:

  1. 更灵活的布局控制(线性、网格、瀑布流等);
  2. 内置高效的 ViewHolder 机制;
  3. 支持动画、分页加载、拖拽排序等高级特性;
  4. 官方已将其作为列表类组件的首选方案。