当前位置: 首页 > news >正文

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() 方法的标准签名是:

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View

其中:

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

🚀 标准复用流程

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

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上来标记处哪些是创建的哪些是复用的。

    /// 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.sizeoverride 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 + statereturn view}}listView.adapter = adapter}

效果如下:

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

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

二、ViewHolder 的作用与实现

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

🎯 问题复盘

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 的标准用法

class ViewHolder(val imageView: ImageView, val textView: TextView)

然后在 getView() 中这样写:

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {val view: Viewval holder: ViewHolderif (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 = convertViewholder = view.tag as ViewHolder}val item = data[position]holder.imageView.setImageResource(item["icon"] as Int)holder.textView.text = item["title"] as Stringreturn view
}

🚀 优化效果

使用 ViewHolder 后:

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

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

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

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

✅ 第一步:定义数据模型

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

data class NewsItem(val title: String,val date: String,val imageResId: Int,var liked: Boolean = false
)

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

放在 res/layout/news_item.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"><ImageViewandroid:id="@+id/image_cover"android:layout_width="match_parent"android:layout_height="180dp"android:scaleType="centerCrop"android:src="@drawable/news_placeholder" /><TextViewandroid: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="新闻标题" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="space_between"android:paddingTop="4dp"><TextViewandroid: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" /><ImageViewandroid:id="@+id/image_like"android:layout_width="24dp"android:layout_height="24dp"android:src="@drawable/ic_like_off" /></LinearLayout>
</LinearLayout>

✅ 第三步:准备数据源

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

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

✅ 第四步:实现列表

/// 使用 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.sizeoverride 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.Viewval holder: NewsViewHolderif (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 = convertViewholder = view.tag as NewsViewHolder}val item = newsList[position]holder.imageView.setImageResource(item.imageResId)holder.titleView.text = item.titleholder.dateView.text = item.dateholder.likeView.setImageResource(if (item.liked) R.drawable.ic_like_on else R.drawable.ic_like_off)return view}}listView.adapter = adapter}

其中NewsViewHolder实现如下:

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. 官方已将其作为列表类组件的首选方案。
http://www.xdnf.cn/news/1240741.html

相关文章:

  • 复现论文《A Fiber Bragg Grating Sensor System for Train Axle Counting》
  • 多级表头的导出
  • 如何使用EF框架操作Sqlite
  • 多租户字典管理系统完整设计
  • TCP 协议的“无消息边界”(No Message Boundaries)特性
  • shell脚本tcpdump抓取数据解析执行关机指令
  • PyCharm代码规范与代码格式化插件安装与使用:pylint和autopep8
  • 质押和抵押有什么区别
  • 【Java】一篇详解HashMap的扩容机制!!
  • 2025年8月4日私鱼创作平台v1.0.4公测版更新发布-完成大部分功能包含关注创作者以及发布作品及合集功能优雅草科技
  • 音视频学习笔记
  • 深入解析 Apache Tomcat 配置文件
  • Planner 5D v2.29.0 安卓高级解锁版,手机3D家装,全套家具免费
  • 鸿蒙开发-端云一体化--云数据库
  • [spring-cloud: 负载均衡]-源码分析
  • Nginx服务做负载均衡网关
  • 【项目实践】在系统接入天气api,根据当前天气提醒,做好plan
  • 基于Java的AI工具和框架
  • 【异常案例分析】使用空指针调用函数(非虚函数)时,没有崩溃在函数调用处,而是崩在被调用函数内部
  • Android Telephony 框架与横向支撑层
  • Android JUnit 测试框架详解:从基础到高级实践
  • Flask + HTML 项目开发思路
  • 开源的现代数据探索和可视化平台:Apache Superset 快速指南 Quickstart
  • Android的UI View是如何最终绘制成一帧显示在手机屏幕上?
  • 阿里云-通义灵码:解锁云原生智能开发新能力,让云开发更“灵”~
  • 福彩双色球第2025089期篮球号码分析
  • 理解 JavaScript 中的“ / ”:路径、资源与目录、nginx配置、请求、转义的那些事
  • 超急评估:用提前计算分摊性能成本
  • go学习笔记:panic是什么含义
  • 工作流绑定卡片优化用户体验-练习我要找工作智能体