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 类会将消息写入 LogcatLogcat 是用于记录消息的控制台。来自 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 是键值对的集合,其中的键始终为字符串。您可以将简单数据(例如 IntBoolean 值)放入 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 中。