第 4 篇|Activity 生命周期与页面跳转 —— 应用的门面担当

2 阅读8分钟

第 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 运行效果

  1. 启动 App → 登录页
  2. 输入「张三」→ 点击登录 → 主页显示「欢迎你,张三」
  3. 点击退出登录 → 返回登录页,输入框已清空
  4. 全程 Logcat 可观察生命周期回调

八、总结与下篇预告

今天的信息量很大,我们来梳理核心收获:

  • ✅ Activity 定义:画框与画纸的关系,ViewRootImpl 充当衔接者
  • ✅ 生命周期:6 核心回调 + 1 补充,理解「创建→可见→交互→失焦→不可见→销毁」
  • ✅ 场景模拟:接电话只触 onPause,切后台走 onStop,旋转屏幕会重建
  • ✅ 页面跳转:显式 Intent 指名道姓,隐式 Intent 按意图匹配
  • ✅ 数据传递:putExtra 传基础数据、@Parcelize 传复杂对象、Activity Result API 获取返回结果
  • ✅ 避坑指南:onPause 不可耗时、屏幕旋转注意数据保存(临时用 onSaveInstanceState,最佳方案是 ViewModel)
  • ✅ 综合实战:登录 + 主页双页面,覆盖跳转、传参、退出全流程

掌握了 Activity,就是掌握了 Android 应用的「门面」。 下一篇,我们将进入 Android 的后台世界,学习 Service(服务)与 BroadcastReceiver(广播接收器),让你的 App 拥有「看不见也能干活」的能力!


✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!