Android 基础知识 02.2:activity 生命周期和状态

1. 欢迎

简介

在本次实践课中,您将详细了解 activity 生命周期。生命周期是 activity 从始至终(从创建到被销毁以及系统收回其资源)可以处于的一组状态。当用户在应用中的 activity 之间导航(以及进入和退出应用)时,这些 activity 都会在它们生命周期的不同状态之间转换。

应用生命周期示意图

activity 生命周期中的每个阶段都有一个对应的回调方法:onCreate()onStart()onPause() 等。当 activity 更改状态时,系统会调用关联的回调方法。您已了解过以下方法:onCreate()。通过替换 Activity 类中的任何生命周期回调方法,您可以更改 activity 的默认行为来响应用户或系统操作。

activity 状态也可能会为响应设备配置变化而更改(例如,当用户将设备从竖屏旋转为横屏时)。当发生这些配置更改时,系统会销毁 activity,并以其默认状态重新创建它,而用户可能会丢失在 activity 中输入的信息。为避免让您的用户感到困惑,请务必在开发应用时做好数据意外丢失防护工作。在本次实践课的后面部分中,您将对配置更改进行实验,了解如何保留为响应设备配置更改和其他 activity 生命周期事件而创建的 activity 的状态。

在本次实践课中,您将日志记录语句添加到 TwoActivities 应用,并在使用该应用时观察 activity 生命周期的变化。然后,开始处理这些更改,并探索在此类情况下如何处理用户输入。

您应当已掌握的内容

您应该能够:

  • 在 Android Studio 中创建并运行应用项目。
  • 将日志语句添加到应用,并在 Logcat 窗格中查看这些日志。
  • 了解和使用 ActivityIntent,并能够轻松自如地与它们互动。

学习内容

  • Activity 生命周期的运作方式。
  • 何时启动、暂停、停止和销毁 Activity
  • Activity 更改相关的生命周期回调方法简介。
  • 可能导致 Activity 生命周期事件的操作(例如配置更改)的影响。
  • 如何在生命周期事件中保留 Activity 状态。

实践内容

  • 将代码添加到上一实践课程中的 TwoActivities 应用,实现各种 Activity 生命周期回调,以添加日志记录语句。
  • 观察在应用运行期间以及您与应用中的每个 Activity 互动时,状态如何变化。
  • 修改应用,以保留为响应用户行为或设备上的配置更改而意外重新创建的 Activity 的实例状态。

2. 应用概览

在本次实践课中,您将向 TwoActivities 应用添加内容。该应用的外观和行为与它在上一个 Codelab 中的外观和行为大致相同。它包含两个 Activity 实现,让用户能够在两者之间发送内容。您在本次实践课中对应用所做的更改不会影响其可见的用户行为。

3. 任务 1:向 TwoActivities 添加生命周期回调

在此任务中,您将实现所有 Activity 生命周期回调方法,以便在调用这些方法时向 Logcat 输出消息。通过这些日志消息,您可以了解 Activity 生命周期状态何时发生更改,以及这些生命周期状态更改会对运行中的应用产生什么影响。

1.1(可选)复制 TwoActivities 项目

对于本次实践课中的任务,您将修改在上一实践课程中构建的现有 TwoActivities 项目。如果您希望将之前的 TwoActivities 项目保持不变,请按照附录:实用程序中的步骤创建项目的副本。

1.2 在 MainActivity 中实现回调

  1. 在 Android Studio 中打开 TwoActivities 项目,然后在 Project > Android 窗格中打开 MainActivity
  2. onCreate() 方法中,添加以下日志语句:
Log.d(LOG_TAG, "-------");
Log.d(LOG_TAG, "onCreate");
  1. onStart() 回调添加替换项,并通过一个语句将该事件记录到日志中:
@Override
public void onStart(){
    super.onStart();
    Log.d(LOG_TAG, "onStart");
}

快捷的方式是在 Android Studio 中依次选择 Code > Override Methods。系统会显示一个对话框,其中列出了您可以在类中替换的所有可能的方法。从列表中选择一个或多个回调方法将会插入这些方法的完整模板,包括对父类的所需调用。

  1. 使用 onStart() 方法作为模板来实现 onPause()onRestart()onResume()onStop()onDestroy() 生命周期回调。

所有回调方法都具有相同的签名(名称除外)。如果您通过复制粘贴 onStart() 创建其他回调方法,请不要忘记更新内容以调用父类中的正确方法,并记录正确的方法。

  1. 运行应用。
  2. 点击 Android Studio 底部的 Logcat 标签页以显示 Logcat 窗格。您应该会看到三条日志消息,这些消息显示了 Activity 从开始后所经历的三种生命周期状态:
D/MainActivity: -------
D/MainActivity: onCreate
D/MainActivity: onStart
D/MainActivity: onResume

1.3 在 SecondActivity 中实现生命周期回调

现在,您已为 MainActivity 实现了生命周期回调方法,请为 SecondActivity 执行相同的操作。

  1. 打开 SecondActivity
  2. 在类的顶部,为 LOG_TAG 变量添加一个常量:
private static final String LOG_TAG = SecondActivity.class.getSimpleName();
  1. 将生命周期回调和日志语句添加到第二个 Activity。(您可以从 MainActivity复制粘贴回调方法。)
  2. 将以下日志语句添加到 returnReply() 方法中的 finish() 方法前面:
Log.d(LOG_TAG, "End SecondActivity");

1.4 在应用运行时观察日志

  1. 运行应用。
  2. 点击 Android Studio 底部的 Logcat 标签页以显示 Logcat 窗格。
  3. 在搜索框中输入 Activity。Android logcat 可能会很长并且杂乱无章。由于每个类中的 LOG_TAG 变量都包含 MainActivitySecondActivity 一词,因此使用该关键字过滤日志可以仅显示您感兴趣的内容。

显示生命周期状态的日志

使用您的应用进行实验,并注意为了响应不同操作而发生的生命周期事件。具体来说,请尝试以下操作:

  • 正常使用应用(发送一条消息,用另一条消息进行回复)。
  • 使用返回按钮从第二个 Activity 返回主 Activity
  • 使用应用栏中的向上箭头从第二个 Activity 返回主 Activity
  • 在不同的时间,在应用中针对主 Activity 和第二个 Activity 旋转设备,并在日志中和屏幕上观察发生的变化。
  • 按“概览”按钮(主屏幕右侧的方形按钮),然后关闭应用(点按 X)。
  • 返回主屏幕,然后重启您的应用。

提示:如果您是在模拟器中运行应用,则可以使用 Control+F11Control+Function+F11 模拟旋转。

任务 1 解决方案代码

以下代码段显示了第一个任务的解决方案代码。

MainActivity

以下代码段显示了 MainActivity 中已添加的代码,但并未显示整个类。

onCreate() 方法:

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Log the start of the onCreate() method.
        Log.d(LOG_TAG, "-------");
        Log.d(LOG_TAG, "onCreate");

        // Initialize all the view variables.
        mMessageEditText = findViewById(R.id.editText_main);
        mReplyHeadTextView = findViewById(R.id.text_header_reply);
        mReplyTextView = findViewById(R.id.text_message_reply);
}

其他生命周期方法:

@Override
protected void onStart() {
        super.onStart();
        Log.d(LOG_TAG, "onStart");
}

@Override
protected void onPause() {
        super.onPause();
        Log.d(LOG_TAG, "onPause");
}

@Override
protected void onRestart() {
        super.onRestart();
        Log.d(LOG_TAG, "onRestart");
}

@Override
protected void onResume() {
        super.onResume();
        Log.d(LOG_TAG, "onResume");
}

@Override
protected void onStop() {
        super.onStop();
        Log.d(LOG_TAG, "onStop");
}

@Override
protected void onDestroy() {
        super.onDestroy();
        Log.d(LOG_TAG, "onDestroy");
}

SecondActivity

以下代码段显示了 SecondActivity 中已添加的代码,但并未显示整个类。

SecondActivity 类的顶部:

private static final String LOG_TAG = SecondActivity.class.getSimpleName();

returnReply() 方法:

public void returnReply(View view) {
        String reply = mReply.getText().toString();
        Intent replyIntent = new Intent();
        replyIntent.putExtra(EXTRA_REPLY, reply);
        setResult(RESULT_OK, replyIntent);
        Log.d(LOG_TAG, "End SecondActivity");
        finish();
}

其他生命周期方法:

与上面的 MainActivity 相同。

4. 任务 2:保存和恢复 Activity 实例状态

根据系统资源和用户行为,应用中每个 Activity 的销毁和重建频率可能会超出您的想象。

您可能在上一部分中旋转设备或模拟器时注意到了此行为。旋转设备就是设备配置更改的一个示例。虽然旋转是最常见的配置更改,但所有配置更改都会导致当前的 Activity 被销毁并重新创建为全新的 Activity。如果您在编写代码时不考虑这种行为,当发生配置更改时,您的 Activity 布局可能会还原为默认外观和初始值,并且您的用户可能会丢失其在应用中的位置、数据或进度状态。

每个 Activity 的状态都以一组键值对的形式存储在名为 Activity 实例状态的 Bundle 对象中。系统会在 Activity 停止之前将默认状态信息保存到实例状态 Bundle,并将该 Bundle 传递给新的 Activity 实例进行恢复。

为了避免 Activity 在被意外销毁并重新创建后丢失数据,您需要实现 onSaveInstanceState() 方法。当系统有可能销毁并重新创建 Activity 时,会对 Activity(介于 onPause()onStop() 之间)调用此方法。

您在实例状态中保存的数据仅特定于当前应用会话期间此具体 Activity 的实例。当您停止并重启新的应用会话后,Activity 实例状态会丢失,并且 Activity 会还原为默认外观。如果您需要在应用会话之间保存用户数据,请使用共享偏好设置或数据库。我们会在后面的实践课程中介绍这两种情况。

2.1 使用 onSaveInstanceState() 保存 Activity 实例状态

您可能已经注意到,旋转设备完全不会影响第二个 Activity 的状态。这是因为第二个 Activity 布局和状态是根据布局和激活它的 Intent 生成的。即使重新创建了 ActivityIntent 也依然存在,并且每次调用第二个 Activity 中的 onCreate() 方法时,仍会使用 Intent 中的数据。

此外,您可能会注意到,在每个 Activity 中,输入到消息或回复 EditText 元素的任何文本都会保留,即使设备旋转也是如此。这是因为,在配置发生变化时,系统会自动保存布局中某些 View 元素的状态信息,而 EditText 的当前值就是其中之一。

因此,您感兴趣的唯一 Activity 状态是回复标题的 TextView 元素和主 Activity 中的回复文本。默认情况下,两个 TextView 元素都不可见;只有当您从第二个 Activity 向主 Activity 发回消息后,这两个元素才会显示。

在此任务中,您将添加代码以使用 onSaveInstanceState() 保留这两个 TextView 元素的实例状态。

  1. 打开 MainActivity
  2. 将此 onSaveInstanceState() 框架实现添加到 Activity,或使用 Code > Override Methods 插入框架替换项。
@Override
public void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
}
  1. 检查标题当前是否可见,如果可见,则使用 putBoolean() 方法和键 "reply_visible" 将该可见性状态置为 Bundle
    if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
        outState.putBoolean("reply_visible", true);
    }

请注意,在第二个 Activity 有回复之前,回复标题和文本将被标记为不可见。如果该标题可见,则表示有需要保存的回复数据。请注意,我们只对可见性状态感兴趣 - 无需保存标题的实际文本,因为文本永远不会更改。

  1. 在同一检查中,将回复文本添加到 Bundle
outState.putString("reply_text",mReplyTextView.getText().toString());

如果标题可见,则可以认为回复消息本身也可见。您无需测试或保存回复消息的当前可见性状态。只有消息的实际文本会进入带有 "reply_text" 键的状态 Bundle

您可以仅保存可能会在创建 Activity 后发生变化的 View 元素的状态。应用中的其他 View 元素(EditTextButton)可以随时从默认布局重新创建。

请注意,系统会保存一些 View 元素的状态,如 EditText 的内容。

2.2 在 onCreate() 中恢复 Activity 实例的状态

保存 Activity 实例状态后,您还需要在重新创建 Activity 后恢复该状态。为此,您可以在 onCreate() 中进行恢复,也可以通过在创建 Activity 后实现 onRestoreInstanceState() 回调(在 onStart() 后调用)进行恢复。

大多数情况下,恢复 Activity 状态的较好位置是在 onCreate() 中,可确保界面(包括状态)尽快可用。有时,在完成所有初始化后,在 onRestoreInstanceState() 中执行此操作非常方便,或者可以让子类决定是否使用您的默认实现。

  1. onCreate() 方法中,使用 findViewById() 初始化 View 变量后,添加一项测试来确保 savedInstanceState 不为 null。
// Initialize all the view variables.
mMessageEditText = findViewById(R.id.editText_main);
mReplyHeadTextView = findViewById(R.id.text_header_reply);
mReplyTextView = findViewById(R.id.text_message_reply);

// Restore the state.
if (savedInstanceState != null) {
}

创建 Activity 后,系统会将状态 Bundle 作为其唯一参数传递给 onCreate()。首次调用 onCreate() 且您的应用启动时,Bundlenull,因为应用首次启动时没有现有状态。对 onCreate() 的后续调用将用您存储在 onSaveInstanceState() 中的数据填充一个捆绑包。

  1. 在该检查中,使用键 "reply_visible"Bundle 中获取当前可见性(true 或 false)。
if (savedInstanceState != null) {
    boolean isVisible =
                     savedInstanceState.getBoolean("reply_visible");
}
  1. 在上一行下方为 isVisible 变量添加一个测试。
if (isVisible) {
}

如果状态 Bundle 中存在 reply_visible 键(因此 isVisibletrue),则需要恢复该状态。

  1. isVisible 测试内,使标题可见。
mReplyHeadTextView.setVisibility(View.VISIBLE);
  1. 从具有 "reply_text" 键的 Bundle 获取文本回复消息,并将回复 TextView 设置为显示该字符串。
mReplyTextView.setText(savedInstanceState.getString("reply_text"));
  1. 将回复 TextView 也设置为可见:
mReplyTextView.setVisibility(View.VISIBLE);
  1. 运行应用。尝试旋转设备或模拟器,确保回复消息(如果有)在重新创建 Activity 后仍然存在。

任务 2 解决方案代码

以下代码段显示了此任务的解决方案代码。

MainActivity

以下代码段显示了 MainActivity 中已添加的代码,但并未显示整个类。

onSaveInstanceState() 方法:

@Override
public void onSaveInstanceState(Bundle outState) {
   super.onSaveInstanceState(outState);
   // If the heading is visible, message needs to be saved.
   // Otherwise we're still using default layout.
   if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
       outState.putBoolean("reply_visible", true);
       outState.putString("reply_text",
                      mReplyTextView.getText().toString());
   }
}

onCreate() 方法:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   Log.d(LOG_TAG, "-------");
   Log.d(LOG_TAG, "onCreate");

   // Initialize all the view variables.
   mMessageEditText = findViewById(R.id.editText_main);
   mReplyHeadTextView = findViewById(R.id.text_header_reply);
   mReplyTextView = findViewById(R.id.text_message_reply);

   // Restore the saved state.
   // See onSaveInstanceState() for what gets saved.
   if (savedInstanceState != null) {
       boolean isVisible =
                     savedInstanceState.getBoolean("reply_visible");
       // Show both the header and the message views. If isVisible is
       // false or missing from the bundle, use the default layout.
       if (isVisible) {
           mReplyHeadTextView.setVisibility(View.VISIBLE);
           mReplyTextView.setText(savedInstanceState
                                  .getString("reply_text"));
           mReplyTextView.setVisibility(View.VISIBLE);
       }
   }
}

完整的项目:

Android Studio 项目:TwoActivitiesLifecycle

5. 编码挑战

挑战:创建一个简单的购物清单应用,其中主 activity 是用户正在构建的清单,第二个 activity 是常见购物清单。

  • 主 activity 应包含要构建的清单,该清单应由 10 个空 TextView 元素组成。
  • 主 activity 上的 Add Item 按钮可启动第二个 activity,其中包含常见购物清单(奶酪大米苹果等等)。使用 Button 元素显示清单项。
  • 用户选择一个清单项后,系统即会返回主 activity,并更新空的 TextView 以包含所选清单项。

使用 Intent 将一个 Activity 中的信息传递到另一个 Activity。确保在用户旋转设备时,系统会保存购物清单的当前状态。

6. 总结

  • Activity 生命周期是 Activity 会经历的一组状态,从首次创建时开始,到 Android 系统回收该 Activity 的资源结束。
  • 当用户从一个 Activity 导航到另一个,以及在应用内外导航时,每个 Activity 都会在 Activity 生命周期中的状态之间移动。
  • Activity 生命周期中的每种状态都有一个对应的回调方法,您可以在 Activity 类中替换此类方法。
  • 生命周期方法包括 onCreate()onStart()onPause()onRestart()onResume()onStop()onDestroy()
  • 通过替换生命周期回调方法,您可以添加在 Activity 转换为该状态时发生的行为。
  • 您可以在 Android Studio 中依次选择 Code > Override,为类添加框架替换方法。
  • 设备配置更改(例如旋转)会导致 Activity 被销毁,并重新创建为新的 Activity。
  • 在发生配置更改时,系统会保留一部分 Activity 状态,包括 EditText 元素的当前值。对于所有其他数据,您必须自行明确保存它们。
  • onSaveInstanceState() 方法中保存 Activity 实例状态。
  • 实例状态数据以简单的键值对形式存储在 Bundle 中。使用 Bundle 方法将数据放入 Bundle 并从中获取数据。
  • 可在 onCreate()(首选方式)或 onRestoreInstanceState() 中恢复实例状态。

7. 相关概念

相关概念文档请参阅 2.2:Activity 生命周期和状态

8. 了解更多内容

Android Studio 文档:

Android 开发者文档:

9. 家庭作业

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测自己的知识掌握情况。

构建并运行应用

  1. 创建一个应用,其布局包括一个计数器 TextView、一个用于递增计数器的 Button 和一个 EditText。请参见下面的屏幕截图示例。您不必精确地复制该布局。
  2. 为递增计数器的 Button 添加点击处理程序。
  3. 运行应用并递增计数器。在 EditText 中输入一些文本。
  4. 旋转设备。请注意,计数器已重置,但 EditText 未重置。
  5. 实现 onSaveInstanceState() 以保存应用的当前状态。
  6. 更新 onCreate() 以恢复应用的状态。
  7. 确保在旋转设备时,应用状态会保留下来。

ebaf84570af6dd46.png

回答以下问题

问题 1

如果您在实现 onSaveInstanceState() 之前运行家庭作业应用,则旋转设备时会发生什么情况?请选择一项:

  • EditText 不再包含您输入的文本,但会保留计数器。
  • 计数器重置为 0,并且 EditText 不再包含您输入的文本。
  • 计数器重置为 0,但 EditText 的内容会保留。
  • 计数器和 EditText 的内容都会保留。

问题 2

当发生设备配置更改(例如旋转)时,系统会调用哪些 Activity 生命周期方法?请选择一项:

  • Android 会通过调用 onStop() 立即关闭您的 Activity。您的代码必须重启 Activity
  • Android 会通过调用 onPause()onStop()onDestroy() 来关闭 Activity。您的代码必须重启 Activity
  • Android 会通过调用 onPause()onStop()onDestroy() 来关闭 Activity,然后通过调用 onCreate()onStart()onResume() 再次启动它。
  • Android 会立即调用 onResume()

问题 3

Activity 生命周期中,何时调用 onSaveInstanceState()?请选择一项:

  • 系统会在 onStop() 方法之前调用 onSaveInstanceState()
  • 系统会在 onResume() 方法之前调用 onSaveInstanceState()
  • 系统会在 onCreate() 方法之前调用 onSaveInstanceState()
  • 系统会在 onDestroy() 方法之前调用 onSaveInstanceState()

问题 4

哪些 Activity 生命周期方法最适合在完成或销毁 Activity 之前保存数据?请选择一项:

  • onPause()onStop()
  • onResume()onCreate()
  • onDestroy()
  • onStart()onRestart()

提交应用以进行评分

评分者的评分指南

检查应用是否具有以下功能:

  • 它会显示一个计数器、一个用于递增该计数器的 Button 和一个 EditText
  • 点击 Button 会使计数器按 1 递增。
  • 当设备旋转时,计数器和 EditText 状态都会保留。
  • MainActivity.java 的实现使用 onSaveInstanceState() 方法来存储计数器值。
  • onCreate() 的实现会测试是否存在 outState Bundle。如果该 Bundle 存在,则恢复计数器值并将其保存到 TextView

10. 下一个 Codelab

如需学习 Android 开发者基础知识 (V2) 课程中的下一个实用 Codelab,请参阅 Android 开发者基础知识 (V2) Codelab

有关本课程的概览,包括概念章节、应用和幻灯片的链接,请参阅 Android 开发者基础知识(版本 2)