activity 生命周期的阶段

简介

在此 Codelab 中,您将详细了解 Android 的基本组成部分:activity。activity 生命周期是 activity 在其整个存在期间可以处于的一组状态。生命周期涵盖从 activity 最初创建一直到它被销毁以及系统收回相应 activity 资源的过程。当用户在应用中的 activity 之间导航(以及进入或退出应用)时,这些 activity 都会在 activity 生命周期的不同状态之间转换。

作为 Android 开发者,您需要了解 activity 生命周期。如果您的 activity 无法正确响应生命周期状态变化,您的应用可能会生成奇怪的错误、出现让您的用户感到困惑的行为,或使用过多的 Android 系统资源。了解 Android 生命周期并正确响应生命周期状态变化,对于成为 Android 优秀公民至关重要。

您应当已掌握的内容

  • 什么是 activity,以及如何在应用中创建 activity。
  • activity 的 onCreate() 方法的用途,以及在该方法中执行的那类操作。

学习内容

  • 如何将日志记录信息输出到 Logcat。
  • Activity 生命周期的基础知识,以及当 activity 在不同状态之间切换时调用的回调。
  • 如何替换生命周期回调方法,以便在 activity 生命周期的不同时间执行操作。

您应执行的操作

  • 修改名为 DessertClicker 的入门应用,以添加在 Logcat 中显示的日志记录信息。
  • 替换生命周期回调方法并记录对 activity 状态的更改。
  • 运行应用,并记下 activity 启动、停止和恢复时显示的日志记录信息。
  • 实现 onSaveInstanceState() 方法,以保留在设备配置变更后可能丢失的应用数据。添加代码,以便在应用再次启动时恢复相应数据。

在此 Codelab 中,您将使用一款名为 DessertClicker 的入门应用。在此应用中,每当用户点按屏幕上的甜点时,该应用就会为用户“购买”相应甜点。应用会在布局中更新已购甜点的数量值,以及用户消费的总金额。

8216c20f5571fc04.png

此应用存在多个与 Android 生命周期有关的错误:例如,在某些情况下,此应用会将甜点数量值重置为 0。了解 Android 生命周期可以帮助您理解这些问题发生的原因以及解决方法。

下载入门应用

下载 DessertClicker 起始代码,并在 Android Studio 中打开它。

如果您使用 GitHub 中的起始代码,请注意文件夹名称为 android-basics-kotlin-dessert-clicker-app-starter。在 Android Studio 中打开项目时,请选择此文件夹。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. 在 Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg 以构建并运行应用。请确保该应用可以正常使用。
  5. 在 Project 工具窗口中浏览项目文件,了解应用的实现方式。

每个 activity 都具有生命周期。这个词源于动植物的生命周期,就像这只蝴蝶的生命周期:蝴蝶的不同状态显示了它从出生到完全成年再到死亡的成长过程。

c685f48ff799f0c9.png

同样,activity 生命周期由 activity 可能会经历的不同状态组成:从 activity 首次初始化,到最终销毁,再由系统收回其内存。当用户启动您的应用、在 activity 之间导航、在应用内外部导航时,activity 会切换状态。下图显示了 activity 生命周期的所有状态。顾名思义,这些状态表示 activity 所处的状态。

c803811f4cb4034b.png

在 activity 生命周期状态发生变化时,您通常需要更改某些行为,或者运行一些代码。因此,Activity 类本身以及 Activity 的任何子类(例如 AppCompatActivity)都会实现一组生命周期回调方法。Android 会在 activity 从一种状态切换为另一种状态时调用这些回调,而您可以在自己的 activity 中替换这些方法,通过执行任务来响应这些生命周期状态变化。下图显示了生命周期状态以及可用的可替换回调。

f6b25a71cec4e401.png

请务必了解要在何时调用这些回调,以及在每个回调方法中应执行的操作。但这两个图都很复杂,可能会令人困惑。在此 Codelab 中,您并不只是要了解每种状态和回调的含义,而是需要进行一些检测工作,逐渐了解发生的情况。

第 1 步:检查 onCreate() 方法并添加日志记录

为了弄清 Android 生命周期的一些情况,了解何时调用各种生命周期方法会很有帮助。这有助于您找出 DessertClicker 中出现问题的地方。

一种简单的方式是使用 Android 日志记录功能。通过日志记录功能,您可以在应用运行期间向控制台写入简短的消息,并利用它们来显示不同回调的触发时间。

  1. 运行 DessertClicker 应用,并在某款甜点的图片上点按多次。请注意 Desserts Sold 的值与美元总金额发生的变化。
  2. 打开 MainActivity.kt 并检查此 activity 的 onCreate() 方法:
override fun onCreate(savedInstanceState: Bundle?) {
...
}

在 activity 生命周期图中,您可能认出了 onCreate() 方法,因为您之前使用过此回调。这便是每个 activity 都必须实现的方法。您可使用 onCreate() 方法为 activity 执行所有一次性初始化。例如,在 onCreate() 中,您可以膨胀布局、定义点击监听器或设置视图绑定。

9be2255ff49e0af8.png

系统会在初始化 activity 之后(在内存中创建新的 Activity 对象后)立即调用一次 onCreate() 生命周期方法。执行 onCreate() 后,相应 activity 会被视为已创建。

  1. 在 onCreate() 方法中,在对 super.onCreate() 的调用后,紧接着添加下面一行代码。
Log.d("MainActivity", "onCreate Called")
  1. 根据需要导入 Log 类(按 Alt+Enter,或者在 Mac 上按 Option+Enter,然后选择 Import)。如果您启用了自动导入功能,系统应该会自动执行此操作。
import android.util.Log

Log 类会将消息写入 Logcat。Logcat 是用于记录消息的控制台。来自 Android 的关于您应用的消息会出现在这里,包括您使用 Log.d() 方法或其他 Log 类方法显式发送到日志的消息。

此命令包含三个部分:

  • 日志消息的优先级,即消息的重要性。在本示例中,Log.d() 方法会写入调试消息。Log 类中的其他方法包括 Log.i()(表示信息性消息)、Log.e()(表示错误)、Log.w()(表示警告)或 Log.v()(表示详细消息)。
  • 日志标签(第一个参数),在本示例中为 "MainActivity"。该标签是一个字符串,可让您更轻松地在 Logcat 中找到自己的日志消息。该标签通常是类的名称。
  • 实际的日志消息(第二个参数)是一个简短的字符串,在本示例中为 "onCreate called"。

编译时常量是个不会改变的值。在变量声明之前使用 const 可将其标记为编译时常量。

  1. 编译并运行 DessertClicker 应用。当您点按甜点后,应用行为没有出现任何变化。在 Android Studio 中,点击屏幕底部的 Logcat 标签页。

ff9c50376701877f.png

  1. 在 Logcat 窗口的搜索字段中输入 D/MainActivity。

bb0b78600cd47789.png

Logcat 可能包含很多消息,大多数消息对您而言都无用。您可以通过多种方式过滤 Logcat 条目,但搜索是最简单的方式。由于您在代码中使用了 MainActivity 作为日志标签,因此您可以使用该标签来过滤日志。在开头添加 D/ 表示这是一条调试消息,由 Log.d() 创建。

您的日志消息包含日期、时间、软件包名称 (com.example.android.dessertclicker)、您的日志标签(开头为 D/)以及实际消息。由于此消息出现在日志中,所以您会知道 onCreate() 已执行。

第 2 步:实现 onStart() 方法

系统会在调用 onCreate() 生命周期方法之后立即调用 onStart()。onStart() 运行后,您的 activity 会显示在屏幕上。与为初始化 activity 而仅调用一次的 onCreate() 不同,onStart() 可在 activity 的生命周期内多次调用。

385df4ce82ae2de9.png

请注意,onStart() 需要与相应的 onStop() 生命周期方法配对使用。如果用户启动您的应用后又返回设备的主屏幕,相应 activity 会停止,并且不会再在屏幕上显示。

  1. 在 Android Studio 中,打开 MainActivity.kt 并将光标移到 MainActivity 类中,依次选择 Code > Override Methods,或按 Control+o(在 Mac 上按 Command+o)。此时,系统会显示一个对话框,其中包含您可以在此类中替换的所有方法的庞大清单。e1f2460242b2ae.png
  2. 开始输入 onStart 以搜索所需的方法。如需滚动到下一个匹配项,请使用向下箭。从列表中选择 onStart(),然后点击 OK 插入样板替换代码。代码如下所示:
override fun onStart() {
   super.onStart()
}
  1. 在 MainActivity.kt 的顶层(也就是类声明 class MainActivity. 的上方)添加以下常量。
const val TAG = "MainActivity"
  1. 在 onStart() 方法中,添加一条日志消息:
override fun onStart() {
   super.onStart()
   Log.d(TAG, "onStart Called")
}
  1. 编译并运行 DessertClicker 应用,然后打开 Logcat 窗格。在搜索字段中输入 D/MainActivity,以过滤日志。请注意,onCreate() 和 onStart() 方法会依次调用,并且您的 activity 会显示在屏幕上。
  2. 按设备上的主屏幕按钮,然后使用“最近使用的应用”屏幕返回相应 activity。请注意,activity 会从上次停止的位置恢复,同时使用所有相同的值,并且 onStart() 会再次记录到 Logcat 中。另请注意,系统通常不会再次调用 onCreate() 方法。
16:19:59.125 31107-31107/com.example.android.dessertclicker D/MainActivity: onCreate Called
16:19:59.372 31107-31107/com.example.android.dessertclicker D/MainActivity: onStart Called
16:20:11.319 31107-31107/com.example.android.dessertclicker D/MainActivity: onStart Called

第 3 步:添加更多日志语句

在此步骤中,您将为所有其他生命周期方法实现日志记录功能。

  1. 替换 MainActivity 中其余的生命周期方法,然后为每种方法添加日志语句。代码如下:
override fun onResume() {
   super.onResume()
   Log.d(TAG, "onResume Called")
}

override fun onPause() {
   super.onPause()
   Log.d(TAG, "onPause Called")
}

override fun onStop() {
   super.onStop()
   Log.d(TAG, "onStop Called")
}

override fun onDestroy() {
   super.onDestroy()
   Log.d(TAG, "onDestroy Called")
}

override fun onRestart() {
   super.onRestart()
   Log.d(TAG, "onRestart Called")
}
  1. 再次编译并运行 DessertClicker,然后检查 Logcat。这一次请注意,除了 onCreate() 和 onStart() 之外,还有一条有关 onResume() 生命周期回调的日志消息。
2020-10-16 10:27:33.244 22064-22064/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 10:27:33.453 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:27:33.454 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called

当 activity 从头开始启动时,您会看到系统按顺序调用以下三个生命周期回调:

  • onCreate():用于创建应用。
  • onStart():用于启动相应 activity,并让其在屏幕上显示。
  • onResume():用于使相应 activity 成为焦点,并让用户能够与其互动。

onResume() 方法尽管名称是这样,但会在启动时调用,即使没有要恢复的 activity 也是如此。

160054d59f67519.png

现在,DessertClicker 应用已设置好日志记录功能,您可以随时通过各种方式开始使用该应用,并且可以探索如何通过触发生命周期回调来响应这些使用行为。

用例 1:打开和关闭 activity

您可以先从最基本的用例入手,也就是首次启动您的应用,然后完全关闭应用。

  1. 编译并运行 DessertClicker 应用(如果该应用尚未运行)。正如您所看到的,当 activity 首次启动时,系统会调用 onCreate()、onStart() 和 onResume() 回调。
2020-10-16 10:27:33.244 22064-22064/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 10:27:33.453 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:27:33.454 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called
  1. 多次点按纸杯蛋糕。
  2. 点按设备上的返回按钮。请注意,在 Logcat 中,系统将按上述顺序调用 onPause()、onStop() 和 onDestroy()。
2020-10-16 10:31:53.850 22064-22064/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 10:31:54.620 22064-22064/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 10:31:54.622 22064-22064/com.example.android.dessertclicker D/MainActivity: onDestroy Called

在本示例中,使用返回按钮会导致 activity(和应用)完全关闭。执行 onDestroy() 方法意味着相应 activity 已完全关闭,可以进行垃圾回收。垃圾回收是指自动清理您不再使用的对象。调用 onDestroy() 后,系统会知道这些资源是可丢弃的,然后开始清理这部分内存。2dcc4d9c6478a9f4.png 如果您的代码手动调用该 activity 的 finish() 方法,或者用户强行退出该应用(例如,用户强行退出,或在“最近使用的应用”屏幕关闭该应用),您的 activity 也可能会完全关闭。如果您的应用长时间没有在屏幕上显示,Android 系统也可能会自行关闭您的 activity。Android 这样做是为了节省电量,同时允许其他应用使用您应用的资源。

  1. 返回 DessertClicker 应用,方法是在概览屏幕中找到所有打开的应用。(注意,这也称为“最近使用的应用”屏幕或“最近用过的应用”)中找到所有打开的应用。以下是 Logcat 中的日志:
2020-10-16 10:31:54.622 22064-22064/com.example.android.dessertclicker D/MainActivity: onDestroy Called
2020-10-16 10:38:00.733 22064-22064/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 10:38:00.787 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:38:00.788 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called

相应 activity 在上一步中已被销毁,因此当您返回该应用时,Android 会启动一个新的 activity 并调用 onCreate()、onStart() 和 onResume() 方法。请注意,前一个 activity 中的 DessertClicker 日志并未保留。

onCreate() 方法是重要的一步;在此方法中,您会执行所有的首次初始化,通过膨胀首次设置其布局,以及对变量进行初始化。

用例 2:离开和返回 activity

现在,您已经启动了应用并将其完全关闭,在此过程中,您看到了 activity 首次创建时经历的大部分生命周期状态。此外,您还看到了 activity 在完全关闭和销毁过程中经历的所有生命周期状态。但当用户与其 Android 设备交互时,他们需要在应用之间切换、返回主屏幕、启动新应用以及处理由通话等其他 activity 导致的中断。

每次用户离开您的 activity 时,它都不会关闭:

  • 当 activity 不再在屏幕上可见时,就说明该 activity 已置于后台(与之相反的是 activity 位于前台或屏幕上)。
  • 当用户返回您的应用时,相应 activity 会重启并再次可见。这部分生命周期称为应用的可见生命周期。

当您的应用位于后台时,为使系统资源和电池续航时间保持正常,应用通常不应活跃运行。您可以使用 Activity 生命周期及其回调来了解应用何时切换到后台,以便您暂停任何正在进行的操作。然后,在您的应用进入前台时重启相应操作。

在这一步中,您将查看当应用进入后台以及返回前台时的 activity 生命周期。

  1. 在 DessertClicker 应用运行时,点击几次纸杯蛋糕。
  2. 按设备上的主屏幕按钮,然后在 Android Studio 中观察 Logcat。返回主屏幕的操作会将您的应用置于后台,而不是完全关闭应用。请注意,系统会调用 onPause() 方法和 onStop() 方法,但不会调用 onDestroy()。
2020-10-16 10:41:05.383 22064-22064/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 10:41:05.966 22064-22064/com.example.android.dessertclicker D/MainActivity: onStop Called

在调用 onPause() 后,该应用不会再获得焦点。在 onStop() 之后,该应用将不再显示在屏幕上。虽然该 activity 已停止,但 Activity 对象仍位于内存中(在后台)。该 activity 尚未销毁。用户可能会返回该应用,因此 Android 会保留您的 activity 资源。b488b32801220b79.png

  1. 使用“最近使用的应用”屏幕返回该应用。请注意,在 Logcat 中,该 activity 使用 onRestart() 和 onStart() 重启,然后使用 onResume() 恢复。
2020-10-16 10:42:18.144 22064-22064/com.example.android.dessertclicker D/MainActivity: onRestart Called
2020-10-16 10:42:18.158 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:42:18.158 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called

当该 activity 返回前台时,系统不会再次调用 onCreate() 方法。相应 activity 对象未被销毁,因此不需要重新创建。系统会调用 onRestart() 方法,而不是 onCreate()。请注意,这一次该 activity 返回前台时,系统会保留 Desserts Sold 数值。

  1. 除 DessertClicker 以外,至少启动一个应用,这样设备的“最近使用的应用”屏幕中就会有一些应用。
  2. 启动“最近使用的应用”屏幕,然后打开另外一个近期 activity。然后,返回最近用过的应用并让 DessertClicker 返回前台。

请注意,您在 Logcat 这里看到的回调与按主屏幕按钮后看到的相同。当应用进入后台时,系统会调用 onPause() 和 onStop(),并在应用返回时调用 onRestart()、onStart() 和 onResume()。

当应用停止并转到后台时,或应用在返回前台后再次启动时,系统会调用这些方法。在这些情况下,如果您需要在应用中执行一些工作,则可以替换相关的生命周期回调方法。

onRestart() 又是什么情况呢?onRestart() 方法与 onCreate() 非常相似。无论是 onCreate() 还是 onRestart(),都会在相应 activity 变得可见之前调用。onCreate() 方法只在第一次被调用,之后会调用 onRestart()。onRestart() 方法用于放置仅在 activity 不是首次启动时才需要调用的代码。

用例 3:隐藏部分 activity

您已了解,在应用启动并且系统调用 onStart() 后,该应用将在屏幕上变得可见。当应用恢复并且系统调用 onResume() 后,应用可获得用户焦点,即,用户可以与应用交互。应用在屏幕上完全显示并且具有用户焦点时的这部分生命周期称为“交互式”生命周期。

当应用进入后台后,焦点在 onPause() 后便会丢失,并且在 onStop() 后,该应用将不再可见。

焦点与可见性之间的区别非常重要,因为 activity 有可能是在屏幕上部分可见,但没有用户焦点。在这一步,您将查看 activity 处于部分可见状态,但没有用户焦点的情况。

  1. 在 DessertClicker 应用运行时,点击屏幕右上角的共享按钮。
  2. 共享 activity 出现在屏幕的下半部分,但相应 activity 在上半部分仍然可见。e2319779260eb5ee.png

9ddc8b1dc79b1bff.png

  1. 检查 Logcat,您会注意到,系统仅调用了 onPause()。
2020-10-16 11:00:53.857 22064-22064/com.example.android.dessertclicker D/MainActivity: onPause Called

在此用例中,系统没有调用 onStop(),因为相应 activity 仍然部分可见。但是,该 activity 没有用户焦点,并且用户无法与之交互;位于前台的“共享”activity 具有用户焦点。

为什么这种区别至关重要?单纯的 onPause() 导致的中断通常会持续很短时间,然后用户就会返回您的 activity,或者导航到另一个 activity 或应用。通常,您需要持续更新界面,使应用的其余部分不会卡顿。

在 onPause() 中运行的任何代码都会阻止其他内容显示,因此请使 onPause() 中的代码保持轻量级。例如,当有来电时,onPause() 中的代码可能会延迟来电通知。

  1. 在共享对话框之外点击一下,返回应用,您会注意到系统调用了 onResume()。

onResume() 和 onPause() 都必须处理焦点。当相应 activity 具有焦点时,系统会调用 onResume() 方法;当该 activity 失去焦点时,系统会调用 onPause()。

activity 生命周期管理中的另一个用例也很重要:配置变更会如何影响 activity 生命周期。

当设备的状态发生了根本性改变,以至于系统解决改变的最简单方式就是完全关闭并重建 activity 时,就会发生配置变更。例如,如果用户更改了设备语言,整个布局可能就需要更改为适应不同的文本方向和字符串长度。如果用户将设备插入基座或添加物理键盘,应用布局可能需要利用不同的显示尺寸或布局。如果设备屏幕方向发生变化,比如设备从纵向旋转为横向或反过来,布局可能需要改为适应新的屏幕方向。让我们看看应用在这种情况下的行为。

设备旋转时出现数据丢失情况

  1. 编译并运行您的应用,然后打开 Logcat。
  2. 将设备或模拟器旋转为横屏模式。您可以使用旋转按钮或 Control 和箭头键(在 Mac 上,则为 Command 和箭头键)来向左或向右旋转模拟器。623fce7c623d42bd.png
  3. 检查 Logcat 中的输出。用 MainActivity 过滤输出。
2020-10-16 11:03:09.618 23206-23206/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 11:03:09.806 23206-23206/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 11:03:09.808 23206-23206/com.example.android.dessertclicker D/MainActivity: onResume Called
2020-10-16 11:03:24.488 23206-23206/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 11:03:24.490 23206-23206/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 11:03:24.493 23206-23206/com.example.android.dessertclicker D/MainActivity: onDestroy Called
2020-10-16 11:03:24.520 23206-23206/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 11:03:24.569 23206-23206/com.example.android.dessertclicker D/MainActivity: onStart Called

请注意,设备或模拟器旋转屏幕时,系统会调用所有生命周期回调来关闭相应 activity。然后,在重新创建 activity 时,系统会调用所有生命周期回调来启动相应 activity。

  1. 当设备旋转,而相应 activity 被关闭并重新创建时,该 activity 会使用默认值启动 - 已售甜点的数量和收入会归零。

使用 onSaveInstanceState() 保存 bundle 数据

onSaveInstanceState() 方法是一个回调,用于保存 Activity 被销毁后可能需要的任何数据。在生命周期回调图中,系统会在相应 activity 停止后调用 onSaveInstanceState()。每当您的应用进入后台时,系统都会调用它。

c259ab6beca0ca88.png

请将 onSaveInstanceState() 调用视为一项安全措施;这让您有机会在相应 activity 退出前台时将少量信息保存到 bundle 中。系统现在会保存这些数据,这是因为如果等到关闭应用时再保存,系统可能会面临资源压力。

每次都保存数据可以确保 bundle 中的更新数据可以根据需要恢复。

  1. 在 MainActivity 中,替换 onSaveInstanceState() 回调,并添加日志语句。
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Log.d(TAG, "onSaveInstanceState Called")
}
  1. 编译并运行应用,然后点击主屏幕按钮,使其进入后台。请注意,onSaveInstanceState() 回调是紧接着 onPause() 和 onStop() 发生的:
2020-10-16 11:05:21.726 23415-23415/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 11:05:22.382 23415-23415/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 11:05:22.393 23415-23415/com.example.android.dessertclicker D/MainActivity: onSaveInstanceState Called
  1. 在文件顶部(就在类定义之前)添加下列常量:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"

您将使用这些键将数据保存到实例状态 bundle 以及从中检索数据。

  1. 向下滚动到 onSaveInstanceState(),您会注意到 outState 参数,其类型为 Bundle。

Bundle 是键值对的集合,其中的键始终为字符串。您可以将简单数据(例如 Int 和 Boolean 值)放入 bundle 中。由于系统会将此 bundle 保留在内存中,因此最佳做法是让 bundle 中的数据量少一点。此 bundle 的大小也有限,但其大小因设备而异。如果您存储的数据过多,可能会面临应用崩溃并出现 TransactionTooLargeException 错误的风险。5. 在 onSaveInstanceState() 中,通过 putInt() 方法将 revenue 值(整数)放入 bundle 中:

outState.putInt(KEY_REVENUE, revenue)

putInt() 方法(以及 Bundle 类中类似的方法,如 putFloat() 和 putString())采用两个参数:键的字符串(KEY_REVENUE 常量)以及要保存的实际值。

  1. 对已售甜点数量重复上述过程:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)

使用 onCreate() 恢复 bundle 数据

activity 状态可以在 onCreate(Bundle) 或 onRestoreInstanceState(Bundle) 中恢复(通过 onSaveInstanceState() 方法填充的 Bundle 将传递给这两种生命周期回调方法)。

  1. 向上滚动到 onCreate(),并检查方法签名:
override fun onCreate(savedInstanceState: Bundle) {

请注意,onCreate() 会在每次调用时都获取 Bundle。当您的 activity 因进程关闭而重启时,您保存的 bundle 会传递给 onCreate()。如果您的 activity 重新启动了,那么 onCreate() 中的这个 Bundle 将是 null。因此,如果 bundle 不是 null,您就会知道自己是在基于一个先前已知的点重新创建该 activity。

  1. 将此代码添加到 onCreate()(紧跟在设置 binding 变量的代码之后):
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

通过测试是否有 null,可以确定 bundle 中是否存在数据,或者 bundle 是否为 null,进而告知您应用是重新启动过,还是在关闭后重新创建过。此测试是从 bundle 恢复数据的常见模式。

请注意,您在此处使用的键 (KEY_REVENUE) 与用于 putInt() 的键相同。为确保每次都使用相同的键,最佳做法是将这些键定义为常量。getInt() 用于从 bundle 中获取数据,就像您之前使用 putInt() 将数据放到 bundle 中一样。getInt() 方法采用两个参数:

  • 一个充当键的字符串,例如 "key_revenue"(表示收入值)。
  • 相应键在 bundle 中不存在值的情况下的默认值。

然后,您从 bundle 获取的整数将分配给 revenue 变量,并且界面将使用该值。

  1. 添加 getInt() 方法,以恢复收入和已售甜点数量。
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
}
  1. 编译并运行应用。按纸杯蛋糕至少五次,直到屏幕画面切换到甜甜圈。
  2. 旋转设备。请注意,这一次,该应用显示的是来自 bundle 的正确收入和已售甜点数量值。另请注意,甜点已恢复为纸杯蛋糕。4179956182ffc634.png 您还需要完成另一项工作,才能确保该应用从关闭状态完全恢复为原来的状态。
  3. 在 MainActivity 中,检查 showCurrentDessert() 方法。请注意,此方法会根据当前已售的甜点数量以及 allDesserts 变量中的甜点清单,决定在 activity 中显示哪张甜点图片。
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

此方法依赖于已售甜点的数量来选择正确的图片。因此,您无需执行任何操作,即可在 onSaveInstanceState() 中将对图片的引用存储到 bundle 内。在该 bundle 中,您已经存储了已售甜点的数量。

  1. 在 onCreate() 中,在用于从 bundle 恢复状态的代码块中,调用 showCurrentDessert():
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   showCurrentDessert()
}
  1. 编译并运行应用,然后旋转屏幕。请注意,现在,已售甜点数量和总收入的值以及甜点图片均已正确恢复。

activity 生命周期

  • activity 生命周期是 activity 会切换的一组状态。 activity 生命周期在 activity 首次创建时开始,到 activity 被销毁时结束。
  • 当用户在 activity 之间以及应用内外导航时,每个 activity 会在 activity 生命周期中的状态之间切换。
  • activity 生命周期中的每种状态都有一个对应的回调方法,您可以在 Activity 类中替换此类方法。核心的生命周期方法集合包括:onCreate()onStart()onPause()onRestart()onResume()onStop()onDestroy()
  • 如需添加要在 activity 转换为某种生命周期状态时发生的行为,请替换相应状态的回调方法。
  • 如需在 Android Studio 中为类添加框架替换方法,请依次选择 Code > Override Methods,或按 Control+o(在 Mac 上按 Command+o)

使用日志进行记录

  • 借助 Android 日志记录 API(尤其是 Log 类),您可以写入要在 Android Studio 内的 Logcat 中显示的简短消息。
  • 使用 Log.d() 可写入调试消息。此方法采用两个参数:日志标签(通常是类的名称)和日志消息(一个简短的字符串)。
  • 使用 Android Studio 中的 Logcat 窗口可查看系统日志,包括您写入的消息。

保留 activity 状态

  • 当您的应用进入后台时(就在系统调用 onStop() 之后),应用数据可保存到 bundle 中。系统会自动为您保存一些应用数据(例如 EditText 的内容)。
  • bundle 是 Bundle 的实例,是键和值的集合。键始终是字符串。
  • 使用 onSaveInstanceState() 回调可将其他数据保存到您要保留的 bundle 中,即使应用自动关闭也是如此。如需将数据放入 bundle,请使用以 put 开头的 bundle 方法,例如 putInt()。
  • 您可以通过 onRestoreInstanceState() 方法从 bundle 中重新获取数据,更常用的方式是使用 onCreate()。onCreate() 方法有一个用于存储 bundle 的 savedInstanceState 参数。
  • 如果 savedInstanceState 变量为 null,则表示 activity 启动时没有状态 bundle,并且没有可以检索的状态数据。
  • 如需使用键从 bundle 中检索数据,请使用以 get 开头的 Bundle 方法,例如 getInt()。

配置变更

  • 当设备的状态发生了根本性改变,以至于系统解决改变的最简单方式就是销毁并重建 activity 时,就会发生配置变更。
  • 最常见的配置变更示例是用户将设备从纵向旋转为横屏模式,或者从横向转为纵向模式。当设备语言发生变化或插入硬件键盘时,也会发生配置变更。
  • 当发生配置变更时,Android 会调用所有 activity 生命周期的关闭回调。然后,Android 会从头开始重启相应 activity,同时运行所有生命周期启动回调。
  • 当 Android 因配置变更而关闭某个应用时,它会使用对 onCreate() 可用的状态 bundle 重启相应 activity。
  • 与进程关闭一样,系统会通过 onSaveInstanceState() 将应用的状态保存到 bundle 中。