About this codelab
1. 欢迎
简介
在此 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()
方法,以保留在设备配置变更后可能丢失的应用数据。添加代码,以便在应用再次启动时恢复相应数据。
2. 应用概览
在此 Codelab 中,您将使用一款名为 DessertClicker 的入门应用。在此应用中,每当用户点按屏幕上的甜点时,该应用就会为用户“购买”相应甜点。应用会在布局中更新已购甜点的数量值,以及用户消费的总金额。
此应用存在多个与 Android 生命周期有关的错误:例如,在某些情况下,此应用会将甜点数量值重置为 0。了解 Android 生命周期可以帮助您理解这些问题发生的原因以及解决方法。
下载入门应用
下载 DessertClicker 起始代码,并在 Android Studio 中打开它。
如果您使用 GitHub 中的起始代码,请注意文件夹名称为 android-basics-kotlin-dessert-clicker-app-starter
。在 Android Studio 中打开项目时,请选择此文件夹。
如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。
获取代码
- 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
- 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。
- 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
- 在计算机上找到该文件(可能在 Downloads 文件夹中)。
- 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
在 Android Studio 中打开项目
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。
注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。
- 在 Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
- 双击该项目文件夹。
- 等待 Android Studio 打开项目。
- 点击 Run 按钮 以构建并运行应用。请确保该应用可以正常使用。
- 在 Project 工具窗口中浏览项目文件,了解应用的实现方式。
3. 探索生命周期方法并添加基本日志记录
每个 activity 都具有生命周期。这个词源于动植物的生命周期,就像这只蝴蝶的生命周期:蝴蝶的不同状态显示了它从出生到完全成年再到死亡的成长过程。
同样,activity 生命周期由 activity 可能会经历的不同状态组成:从 activity 首次初始化,到最终销毁,再由系统收回其内存。当用户启动您的应用、在 activity 之间导航、在应用内外部导航时,activity 会切换状态。下图显示了 activity 生命周期的所有状态。顾名思义,这些状态表示 activity 所处的状态。
在 activity 生命周期状态发生变化时,您通常需要更改某些行为,或者运行一些代码。因此,Activity
类本身以及 Activity
的任何子类(例如 AppCompatActivity
)都会实现一组生命周期回调方法。Android 会在 activity 从一种状态切换为另一种状态时调用这些回调,而您可以在自己的 activity 中替换这些方法,通过执行任务来响应这些生命周期状态变化。下图显示了生命周期状态以及可用的可替换回调。
请务必了解要在何时调用这些回调,以及在每个回调方法中应执行的操作。但这两个图都很复杂,可能会令人困惑。在此 Codelab 中,您并不只是要了解每种状态和回调的含义,而是需要进行一些检测工作,逐渐了解发生的情况。
第 1 步:检查 onCreate() 方法并添加日志记录
为了弄清 Android 生命周期的一些情况,了解何时调用各种生命周期方法会很有帮助。这有助于您找出 DessertClicker 中出现问题的地方。
一种简单的方式是使用 Android 日志记录功能。通过日志记录功能,您可以在应用运行期间向控制台写入简短的消息,并利用它们来显示不同回调的触发时间。
- 运行 DessertClicker 应用,并在某款甜点的图片上点按多次。请注意 Desserts Sold 的值与美元总金额发生的变化。
- 打开
MainActivity.kt
并检查此 activity 的onCreate()
方法:
override fun onCreate(savedInstanceState: Bundle?) {
...
}
在 activity 生命周期图中,您可能认出了 onCreate()
方法,因为您之前使用过此回调。这便是每个 activity 都必须实现的方法。您可使用 onCreate()
方法为 activity 执行所有一次性初始化。例如,在 onCreate()
中,您可以膨胀布局、定义点击监听器或设置视图绑定。
系统会在初始化 activity 之后(在内存中创建新的 Activity
对象后)立即调用一次 onCreate()
生命周期方法。执行 onCreate()
后,相应 activity 会被视为已创建。
- 在
onCreate()
方法中,在对super.onCreate()
的调用后,紧接着添加下面一行代码。
Log.d("MainActivity", "onCreate Called")
- 根据需要导入
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
可将其标记为编译时常量。
- 编译并运行 DessertClicker 应用。当您点按甜点后,应用行为没有出现任何变化。在 Android Studio 中,点击屏幕底部的 Logcat 标签页。
- 在 Logcat 窗口的搜索字段中输入
D/MainActivity
。
Logcat 可能包含很多消息,大多数消息对您而言都无用。您可以通过多种方式过滤 Logcat 条目,但搜索是最简单的方式。由于您在代码中使用了 MainActivity
作为日志标签,因此您可以使用该标签来过滤日志。在开头添加 D/
表示这是一条调试消息,由 Log.d()
创建。
您的日志消息包含日期、时间、软件包名称 (com.example.android.dessertclicker
)、您的日志标签(开头为 D/
)以及实际消息。由于此消息出现在日志中,所以您会知道 onCreate()
已执行。
第 2 步:实现 onStart() 方法
系统会在调用 onCreate()
生命周期方法之后立即调用 onStart()
。onStart()
运行后,您的 activity 会显示在屏幕上。与为初始化 activity 而仅调用一次的 onCreate()
不同,onStart()
可在 activity 的生命周期内多次调用。
请注意,onStart()
需要与相应的 onStop()
生命周期方法配对使用。如果用户启动您的应用后又返回设备的主屏幕,相应 activity 会停止,并且不会再在屏幕上显示。
- 在 Android Studio 中,打开
MainActivity.kt
并将光标移到MainActivity
类中,依次选择 Code > Override Methods,或按Control+o
(在 Mac 上按Command+o
)。此时,系统会显示一个对话框,其中包含您可以在此类中替换的所有方法的庞大清单。 - 开始输入
onStart
以搜索所需的方法。如需滚动到下一个匹配项,请使用向下箭。从列表中选择onStart()
,然后点击 OK 插入样板替换代码。代码如下所示:
override fun onStart() {
super.onStart()
}
- 在
MainActivity.kt
的顶层(也就是类声明class
MainActivity.
的上方)添加以下常量。
const val TAG = "MainActivity"
- 在
onStart()
方法中,添加一条日志消息:
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart Called")
}
- 编译并运行 DessertClicker 应用,然后打开 Logcat 窗格。在搜索字段中输入
D/MainActivity
,以过滤日志。请注意,onCreate()
和onStart()
方法会依次调用,并且您的 activity 会显示在屏幕上。 - 按设备上的主屏幕按钮,然后使用“最近使用的应用”屏幕返回相应 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 步:添加更多日志语句
在此步骤中,您将为所有其他生命周期方法实现日志记录功能。
- 替换
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")
}
- 再次编译并运行 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 也是如此。
4. 探索生命周期用例
现在,DessertClicker 应用已设置好日志记录功能,您可以随时通过各种方式开始使用该应用,并且可以探索如何通过触发生命周期回调来响应这些使用行为。
用例 1:打开和关闭 activity
您可以先从最基本的用例入手,也就是首次启动您的应用,然后完全关闭应用。
- 编译并运行 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
- 多次点按纸杯蛋糕。
- 点按设备上的返回按钮。请注意,在 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()
后,系统会知道这些资源是可丢弃的,然后开始清理这部分内存。 如果您的代码手动调用该 activity 的 finish()
方法,或者用户强行退出该应用(例如,用户强行退出,或在“最近使用的应用”屏幕关闭该应用),您的 activity 也可能会完全关闭。如果您的应用长时间没有在屏幕上显示,Android 系统也可能会自行关闭您的 activity。Android 这样做是为了节省电量,同时允许其他应用使用您应用的资源。
- 返回 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 生命周期。
- 在 DessertClicker 应用运行时,点击几次纸杯蛋糕。
- 按设备上的主屏幕按钮,然后在 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 资源。
- 使用“最近使用的应用”屏幕返回该应用。请注意,在 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 数值。
- 除 DessertClicker 以外,至少启动一个应用,这样设备的“最近使用的应用”屏幕中就会有一些应用。
- 启动“最近使用的应用”屏幕,然后打开另外一个近期 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 处于部分可见状态,但没有用户焦点的情况。
- 在 DessertClicker 应用运行时,点击屏幕右上角的共享按钮。
- 共享 activity 出现在屏幕的下半部分,但相应 activity 在上半部分仍然可见。
- 检查 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()
中的代码可能会延迟来电通知。
- 在共享对话框之外点击一下,返回应用,您会注意到系统调用了
onResume()
。
onResume()
和 onPause()
都必须处理焦点。当相应 activity 具有焦点时,系统会调用 onResume()
方法;当该 activity 失去焦点时,系统会调用 onPause()
。
5. 探索配置变更
activity 生命周期管理中的另一个用例也很重要:配置变更会如何影响 activity 生命周期。
当设备的状态发生了根本性改变,以至于系统解决改变的最简单方式就是完全关闭并重建 activity 时,就会发生配置变更。例如,如果用户更改了设备语言,整个布局可能就需要更改为适应不同的文本方向和字符串长度。如果用户将设备插入基座或添加物理键盘,应用布局可能需要利用不同的显示尺寸或布局。如果设备屏幕方向发生变化,比如设备从纵向旋转为横向或反过来,布局可能需要改为适应新的屏幕方向。让我们看看应用在这种情况下的行为。
设备旋转时出现数据丢失情况
- 编译并运行您的应用,然后打开 Logcat。
- 将设备或模拟器旋转为横屏模式。您可以使用旋转按钮或
Control
和箭头键(在 Mac 上,则为Command
和箭头键)来向左或向右旋转模拟器。 - 检查 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。
- 当设备旋转,而相应 activity 被关闭并重新创建时,该 activity 会使用默认值启动 - 已售甜点的数量和收入会归零。
使用 onSaveInstanceState() 保存 bundle 数据
onSaveInstanceState()
方法是一个回调,用于保存 Activity
被销毁后可能需要的任何数据。在生命周期回调图中,系统会在相应 activity 停止后调用 onSaveInstanceState()
。每当您的应用进入后台时,系统都会调用它。
请将 onSaveInstanceState()
调用视为一项安全措施;这让您有机会在相应 activity 退出前台时将少量信息保存到 bundle 中。系统现在会保存这些数据,这是因为如果等到关闭应用时再保存,系统可能会面临资源压力。
每次都保存数据可以确保 bundle 中的更新数据可以根据需要恢复。
- 在
MainActivity
中,替换onSaveInstanceState()
回调,并添加日志语句。
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log.d(TAG, "onSaveInstanceState Called")
}
- 编译并运行应用,然后点击主屏幕按钮,使其进入后台。请注意,
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
- 在文件顶部(就在类定义之前)添加下列常量:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
您将使用这些键将数据保存到实例状态 bundle 以及从中检索数据。
- 向下滚动到
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
常量)以及要保存的实际值。
- 对已售甜点数量重复上述过程:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
使用 onCreate() 恢复 bundle 数据
activity 状态可以在 onCreate(Bundle)
或 onRestoreInstanceState(Bundle)
中恢复(通过 onSaveInstanceState()
方法填充的 Bundle
将传递给这两种生命周期回调方法)。
- 向上滚动到
onCreate()
,并检查方法签名:
override fun onCreate(savedInstanceState: Bundle) {
请注意,onCreate()
会在每次调用时都获取 Bundle
。当您的 activity 因进程关闭而重启时,您保存的 bundle 会传递给 onCreate()
。如果您的 activity 重新启动了,那么 onCreate()
中的这个 Bundle
将是 null
。因此,如果 bundle 不是 null
,您就会知道自己是在基于一个先前已知的点重新创建该 activity。
- 将此代码添加到
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
变量,并且界面将使用该值。
- 添加
getInt()
方法,以恢复收入和已售甜点数量。
if (savedInstanceState != null) {
revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
}
- 编译并运行应用。按纸杯蛋糕至少五次,直到屏幕画面切换到甜甜圈。
- 旋转设备。请注意,这一次,该应用显示的是来自 bundle 的正确收入和已售甜点数量值。另请注意,甜点已恢复为纸杯蛋糕。 您还需要完成另一项工作,才能确保该应用从关闭状态完全恢复为原来的状态。
- 在
MainActivity
中,检查showCurrentDessert()
方法。请注意,此方法会根据当前已售的甜点数量以及allDesserts
变量中的甜点清单,决定在 activity 中显示哪张甜点图片。
for (dessert in allDesserts) {
if (dessertsSold >= dessert.startProductionAmount) {
newDessert = dessert
}
else break
}
此方法依赖于已售甜点的数量来选择正确的图片。因此,您无需执行任何操作,即可在 onSaveInstanceState()
中将对图片的引用存储到 bundle 内。在该 bundle 中,您已经存储了已售甜点的数量。
- 在
onCreate()
中,在用于从 bundle 恢复状态的代码块中,调用showCurrentDessert()
:
if (savedInstanceState != null) {
revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
showCurrentDessert()
}
- 编译并运行应用,然后旋转屏幕。请注意,现在,已售甜点数量和总收入的值以及甜点图片均已正确恢复。
6. 总结
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 中。