第 4 篇|Activity 生命周期与页面跳转 —— 应用的门面担当
掌握 Activity 核心用法,搞定生命周期、页面跳转与数据传递
哈喽,各位坚持学习的小伙伴们!上一篇我们搭建了第一个 Android 界面,成功让「Hello, CSDN!」跑了起来。今天,我们要深入 Android 最核心的组件 —— Activity,它被称为 Android 四大组件之首,是你开发中打交道最多的东西。
如果把 App 比作一个舞台,Activity 就是一块块不同的幕布。用户在手机上看到的每一个全屏界面,几乎都对应着一个 Activity。这一篇,我们将彻底搞懂 Activity 的生命周期规律,学会如何在页面之间自由跳转并传递数据。
全程附可运行代码 + 避坑指南,我们直接开始!
一、Activity 是什么?它与 View 的关系
1.1 Activity 的通俗定义
Activity 是 Android 应用中最基本的组件之一,负责承载和展示用户界面,并与用户进行交互。
通俗地讲,Activity 就是手机上占据整个屏幕的一个「页面」。比如你打开微信,首先看到的聊天列表是一个 Activity;点击某个联系人,进入聊天窗口,那是另一个 Activity。
1.2 Activity 与 View 的关系 —— 「画框」与「画纸」
很多新手搞不清 Activity 和 View 是什么关系,我们用一句大白话讲清楚:
Activity 是画框,View 是画纸。Activity 负责管理生命周期,View 负责在屏幕上画东西。
- Activity:负责接收系统事件(如页面打开、关闭、切后台),协调资源和生命周期。它本身并不在屏幕上显示任何东西。
- View:所有你能看到的 UI 元素(按钮、文本框、图片)都是 View。布局文件(XML)最终会被解析成 View 树,挂载到 Activity 上。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView 就是把 View 挂到 Activity 上,完成“画纸装裱进画框”
setContentView(R.layout.activity_main)
}
}
1.3 更深一层:ViewRootImpl 衔接者
实际上 Activity 并不直接绘制 UI,两者之间还有一个隐形的桥梁 —— ViewRootImpl。Activity 的 onResume() 执行完毕后,系统会创建 ViewRootImpl,由它触发 View 树的 measure(测量)→ layout(定位)→ draw(绘制) 全流程,并负责把用户的触摸事件反馈给 View。
类比助记:
- Activity = 「店铺老板」:管开业、营业、歇业
- View = 「商品」:展示并被用户挑选
- ViewRootImpl = 「店员」:按老板指令摆放商品、接待顾客
这样理解,三者的分工就一目了然了。
二、生命周期全图解 —— 官方逻辑一网打尽
Activity 从创建到销毁,会经历一系列状态回调,这就是生命周期。它的生命周期完全由系统控制,我们无法主动创建或销毁 Activity,但可以通过重写回调方法,在合适的时机做合适的操作。
2.1 核心生命周期回调(6 个核心 + 1 个补充)
简化流程图:
onCreate → onStart → onResume → (运行态)
↓
onPause → (部分可见)
↓
onStop → (不可见)
↓
onDestroy
补充:onStop → onRestart → onStart → onResume
| 回调方法 | 触发时机 | 核心作用 | 注意事项 |
|---|---|---|---|
| onCreate() | Activity 首次创建(仅一次),或屏幕旋转后重建 | 加载布局、初始化控件、绑定数据 | 不可做耗时操作,否则启动卡顿 |
| onStart() | 界面即将可见(可能多次) | 注册广播、传感器等全局监听 | 此时还不能交互,仅视觉上可见 |
| onResume() | 界面获得焦点,可交互(可能多次 | ) 启动动画、恢复暂停的视频播放 | 前台活跃态,核心交互阶段 |
| onPause() | 失去焦点但可能仍可见 | 快速保存草稿、暂停动画/传感器 | ⚠️ 严禁耗时操作,否则会卡顿或 ANR |
| onStop() | 完全不可见 | 释放较重资源(关闭数据库连接等) | 系统可能在此后回收 Activity |
| onDestroy() | Activity 被销毁前 | 最终清理:取消广播、解绑服务 | 不可再操作 UI |
| onRestart() | 从 onStop 重新回到前台 | 无需额外操作,之后走 onStart | 仅在“切后台后重新打开”时触发 |
2.2 实战验证:用 Log 观察生命周期
建议新手亲自打印日志,感受回调顺序:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d("Lifecycle", "onCreate 调用 —— 界面首次创建")
}
override fun onStart() {
super.onStart()
Log.d("Lifecycle", "onStart 调用 —— 界面即将可见")
}
override fun onResume() {
super.onResume()
Log.d("Lifecycle", "onResume 调用 —— 界面获得焦点,可交互")
}
override fun onPause() {
super.onPause()
Log.d("Lifecycle", "onPause 调用 —— 失去焦点")
}
override fun onStop() {
super.onStop()
Log.d("Lifecycle", "onStop 调用 —— 完全不可见")
}
override fun onDestroy() {
super.onDestroy()
Log.d("Lifecycle", "onDestroy 调用 —— 界面被销毁")
}
override fun onRestart() {
super.onRestart()
Log.d("Lifecycle", "onRestart 调用 —— 从不可见重新恢复")
}
}
运行后,在 AS 底部 Logcat 面板中过滤 Lifecycle,观察不同操作下的日志输出。
三、场景模拟 —— 接电话、切后台、旋转屏幕
结合实际场景才能吃透生命周期。下面 3 个最常见场景,新手务必动手验证。
场景 1:接电话(失去焦点但可见)
操作:正在用 App → 来电弹窗出现 → 挂断电话。
生命周期:
onResume → onPause (来电, 失去焦点但仍可见) → onResume (挂断, 重新获得焦点)
关键点:此时 Activity 未完全不可见,所以不触发 onStop。可在 onPause 中快速保存草稿。
场景 2:切后台(完全不可见)
操作:按 Home 键切后台 → 从最近任务重新打开。
生命周期:
切后台: onPause → onStop
回到前台: onRestart → onStart → onResume
关键点:重新打开时通过 onRestart 恢复,不走 onCreate。若后台时间过长,系统可能直接 onDestroy。
场景 3:屏幕旋转(销毁并重建)
操作:竖屏使用 → 旋转手机。
生命周期:
onPause → onStop → onDestroy → onCreate → onStart → onResume
关键点:这是一个大坑!Activity 会被完全销毁再重建。如果不做特殊处理,表单输入、列表数据等全部丢失。后面会讲解决方案。
四、页面跳转 —— Intent 显式 / 隐式跳转
Android 中页面跳转依赖 Intent(意图),它就像「信使」,负责告知系统要干什么。
4.1 显式 Intent —— 明确指定目标页面(应用内部)
显式跳转直接指明目标 Activity 的类名,适用于应用内部页面切换。
前置准备:创建 SecondActivity(右键包名 → New → Activity → Empty Views Activity),AS 会自动生成布局和类文件,并在 Manifest 中注册。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btnJump = findViewById<Button>(R.id.btn_jump)
btnJump.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
// 可选:跳转后关闭当前页(如登录成功后不再返回)
// finish()
}
}
}
4.2 隐式 Intent —— 按意图匹配页面(跨应用)
不指定具体类名,而是通过 Action + Data 让系统找到能处理的组件。常用于打开系统功能。
案例 1:打开浏览器
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.csdn.net")
startActivity(intent) // 系统会弹出浏览器列表让用户选择
案例 2:打开拨号盘
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
案例 3:发送短信
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("smsto:10086")
intent.putExtra("sms_body", "你好,这是一条测试短信")
startActivity(intent)
隐式 Intent 原理:系统根据 Intent 的 Action、Data 等属性,去匹配 AndroidManifest.xml 中声明了对应 < intent-filter > 的组件。若有多个匹配,让用户选择。
五、数据传递 —— putExtra、@Parcelize 与 Activity Result API
页面跳转时经常需要带数据。我们从基础到现代,逐一讲解。
5.1 传统方式:putExtra + getIntent(基础类型)
发送方(MainActivity):
val intent = Intent(this, SecondActivity::class.java).apply {
putExtra("username", "张三")
putExtra("age", 25)
putExtra("isVip", true)
}
startActivity(intent)
接收方(SecondActivity):
val username = intent.getStringExtra("username") ?: "未命名"
val age = intent.getIntExtra("age", 0)
val isVip = intent.getBooleanExtra("isVip", false)
5.2 传递复杂对象:@Parcelize 序列化
自定义对象需要序列化才能传递。Kotlin 的 @Parcelize 让这个过程极简。
步骤 1:在 app/build.gradle.kts 中开启支持:
plugins {
id("kotlin-parcelize")
}
步骤 2:定义数据类:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class User(
val name: String,
val age: Int,
val email: String
) : Parcelable
步骤 3:传递与接收:
// 发送
val user = User("张三", 25, "zhangsan@csdn.com")
val intent = Intent(this, SecondActivity::class.java).apply {
putExtra("user", user)
}
startActivity(intent)
// 接收
val user = intent.getParcelableExtra<User>("user")
5.3 现代方式:Activity Result API(推荐获取返回结果)
传统的 startActivityForResult 已废弃。Google 推荐 Activity Result API,更安全、解耦。
场景:从设置页修改用户名后,返回主页刷新显示。
主页(MainActivity)注册回调:
class MainActivity : AppCompatActivity() {
private val getUserNameResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val newName = result.data?.getStringExtra("newUserName")
Toast.makeText(this, "收到新用户名:$newName", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn_goto_setting).setOnClickListener {
val intent = Intent(this, SettingActivity::class.java)
getUserNameResult.launch(intent)
}
}
}
设置页(SettingActivity)返回数据:
findViewById<Button>(R.id.btn_save).setOnClickListener {
val newName = findViewById<EditText>(R.id.et_username).text.toString()
val intent = Intent().apply {
putExtra("newUserName", newName)
}
setResult(Activity.RESULT_OK, intent)
finish()
}
优势:无需手动管理 requestCode,类型安全,回调逻辑更清晰。
六、新手必踩坑点清单
坑点 ①:在 onPause 中做耗时操作,导致页面切换卡顿
错误示范:
override fun onPause() {
super.onPause()
// ❌ 错误!在 onPause 中执行耗时操作
Thread.sleep(2000)
saveAllDataToFile()
}
后果:onPause 执行完毕后,下一个页面的 onResume 才会执行。耗时操作会导致页面切换明显卡顿,甚至触发 ANR(应用无响应)。
正确做法:
onPause 仅做轻量级操作:暂停动画、停止传感器、提交轻量 SharedPreferences
耗时操作(写数据库、网络请求)放到 onStop 或后台线程
坑点 ②:屏幕旋转数据丢失
如前文所述,旋转屏幕会导致 Activity 销毁并重建,内存中的临时数据全部清空。
临时解法(简单场景可用):
// 保存数据(旋转前触发)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("inputText", etUsername.text.toString())
}
// 恢复数据(onCreate 中)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
savedInstanceState?.let {
val savedText = it.getString("inputText", "")
etUsername.setText(savedText)
}
}
最佳方案:后续将学习的 ViewModel。它会自动处理配置变更,即使 Activity 重建,ViewModel 中的数据也不会丢失,比 onSaveInstanceState 更优雅、更强大。
七、综合小案例 —— 登录 + 主页双页面
把今天所有知识串起来:登录页输入用户名 → 跳转主页显示欢迎语 → 退出登录返回登录页并清空输入框。
7.1 布局文件
登录页 activity_login.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户登录"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="80dp" />
<EditText
android:id="@+id/et_username"
android:layout_width="0dp"
android:layout_height="56dp"
android:hint="请输入用户名"
android:textSize="16sp"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toBottomOf="@id/tv_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="40dp" />
<Button
android:id="@+id/btn_login"
android:layout_width="0dp"
android:layout_height="56dp"
android:text="登 录"
android:textSize="18sp"
app:layout_constraintTop_toBottomOf="@id/et_username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
主页 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<TextView
android:id="@+id/tv_welcome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="26sp"
android:textStyle="bold"
android:textColor="#333333"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="120dp" />
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登录成功!欢迎回来"
android:textSize="16sp"
android:textColor="#666666"
app:layout_constraintTop_toBottomOf="@id/tv_welcome"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/btn_logout"
android:layout_width="0dp"
android:layout_height="56dp"
android:text="退出登录"
android:textSize="18sp"
android:textColor="#FFFFFF"
android:backgroundTint="#E53935"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="48dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
7.2 Activity 代码
登录页 LoginActivity.kt(使用 Activity Result API 接收退出状态,并清空输入框):
class LoginActivity : AppCompatActivity() {
private lateinit var etUsername: EditText
private lateinit var btnLogin: Button
// 注册回调,接收主页退出返回的结果
private val logoutResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
etUsername.setText("") // 退出登录后清空输入框
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
Log.d("LoginActivity", "onCreate")
etUsername = findViewById(R.id.et_username)
btnLogin = findViewById(R.id.btn_login)
btnLogin.setOnClickListener {
val username = etUsername.text.toString().trim()
if (username.isBlank()) {
Toast.makeText(this, "请输入用户名", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("username", username)
}
logoutResult.launch(intent) // 用启动器跳转
}
}
}
主页 MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var tvWelcome: TextView
private lateinit var btnLogout: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d("MainActivity", "onCreate")
tvWelcome = findViewById(R.id.tv_welcome)
btnLogout = findViewById(R.id.btn_logout)
val username = intent.getStringExtra("username") ?: "未知用户"
tvWelcome.text = "欢迎你,$username"
btnLogout.setOnClickListener {
val resultIntent = Intent().apply {
putExtra("logout_user", username)
}
setResult(Activity.RESULT_OK, resultIntent)
finish() // 关闭主页,返回登录页
}
}
}
7.3 配置 AndroidManifest.xml
把 LoginActivity 设为启动页:
<application ...>
<activity
android:name=".MainActivity"
android:exported="false" />
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
7.4 运行效果
- 启动 App → 登录页
- 输入「张三」→ 点击登录 → 主页显示「欢迎你,张三」
- 点击退出登录 → 返回登录页,输入框已清空
- 全程 Logcat 可观察生命周期回调
八、总结与下篇预告
今天的信息量很大,我们来梳理核心收获:
- ✅ Activity 定义:画框与画纸的关系,ViewRootImpl 充当衔接者
- ✅ 生命周期:6 核心回调 + 1 补充,理解「创建→可见→交互→失焦→不可见→销毁」
- ✅ 场景模拟:接电话只触 onPause,切后台走 onStop,旋转屏幕会重建
- ✅ 页面跳转:显式 Intent 指名道姓,隐式 Intent 按意图匹配
- ✅ 数据传递:putExtra 传基础数据、@Parcelize 传复杂对象、Activity Result API 获取返回结果
- ✅ 避坑指南:onPause 不可耗时、屏幕旋转注意数据保存(临时用 onSaveInstanceState,最佳方案是 ViewModel)
- ✅ 综合实战:登录 + 主页双页面,覆盖跳转、传参、退出全流程
掌握了 Activity,就是掌握了 Android 应用的「门面」。 下一篇,我们将进入 Android 的后台世界,学习 Service(服务)与 BroadcastReceiver(广播接收器),让你的 App 拥有「看不见也能干活」的能力!
✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!