某些设备配置可在应用运行时发生变化。其中包括但不限于
- 应用显示尺寸
- 屏幕方向
- 字体大小和字重
- 区域设置
- 深色模式与浅色模式
- 键盘可用性
大多数这些配置变更都是由用户互动引起的。例如,旋转或折叠设备会改变应用可用的屏幕空间量。同样,更改设备设置(例如字体大小、语言或首选主题)也会改变 Configuration
对象中对应的相应值。
这些参数通常要求对应用界面进行足够大的更改,以至于 Android 平台为此类更改提供了专门构建的机制。此机制是 Activity
重建。
Activity 重建
当发生配置变更时,系统会重建 Activity
。为此,系统会调用 onDestroy()
并销毁现有的 Activity
实例。然后,它会使用 onCreate()
创建一个新实例,并且此新的 Activity
实例会使用新的、更新的配置进行初始化。这也意味着系统也会使用新配置重建 UI。
重建行为有助于您的应用适应新配置,方法是自动使用与新设备配置匹配的备用资源重新加载您的应用。
重建示例
考虑一个 TextView
,它使用布局 XML 文件中定义的 android:text="@string/title"
显示一个静态标题。当视图创建时,它会根据当前语言精确设置文本一次。如果语言发生变化,系统会重建 Activity。因此,系统也会重建视图并根据新语言将其初始化为正确的值。
重建还会清除作为 Activity
或其包含的任何 Fragment
、View
或其他对象中的字段保留的任何状态。这是因为 Activity
重建会创建一个全新的 Activity
实例和 UI。此外,旧的 Activity
不再可见或有效,因此任何对其或其包含对象的剩余引用都是陈旧的。它们可能导致错误、内存泄漏和崩溃。
用户预期
应用的用户期望状态得到保留。如果用户正在填写表单,并在多窗口模式下打开另一个应用以查阅信息,而当他们返回时发现表单已清空或回到了应用的完全不同位置,这将是糟糕的用户体验。作为开发者,您必须在配置变更和 Activity 重建过程中提供一致的用户体验。
为了验证应用中的状态是否得到保留,您可以执行导致配置变更的操作,无论应用在前台还是后台。这些操作包括
- 旋转设备
- 进入多窗口模式
- 在多窗口模式或自由形式窗口中调整应用大小
- 折叠带多个显示屏的折叠屏设备
- 更改系统主题,例如深色模式与浅色模式
- 更改字体大小
- 更改系统或应用语言
- 连接或断开硬件键盘
- 连接或断开扩展坞
有三种主要方法可以保留 Activity
重建时的相关状态。使用哪种方法取决于您要保留的状态类型
- 使用本地持久化来处理复杂或大量数据的进程终止。持久化本地存储包括数据库或
DataStore
。 - 使用保留对象(例如
ViewModel
实例)在用户积极使用应用时处理内存中的 UI 相关状态。 - 使用保存实例状态来处理系统发起的进程终止,并保留依赖于用户输入或导航的瞬态状态。
要详细了解每种 API 以及何时适合使用,请参阅保存 UI 状态。
限制 Activity 重建
您可以阻止某些配置变更导致 Activity 自动重建。Activity
重建会导致整个 UI 以及从 Activity
派生的任何对象被重建。您可能有充分的理由避免这种情况。例如,您的应用在特定配置变更期间可能不需要更新资源,或者可能存在性能限制。在这种情况下,您可以声明您的 Activity 自己处理配置变更,并阻止系统重启您的 Activity。
要禁用特定配置变更的 Activity 重建,请将配置类型添加到 AndroidManifest.xml
文件中 <activity>
条目中的 android:configChanges
。可能的值显示在 android:configChanges
属性的文档中。
以下清单代码禁用 MyActivity
在屏幕方向和键盘可用性变更时的 Activity
重建
<activity
android:name=".MyActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:label="@string/app_name">
某些配置变更总是会导致 Activity 重启。您无法禁用它们。例如,您无法禁用 Android 12L (API 级别 32) 中引入的动态颜色变更。
在 View 系统中响应配置变更
在 View
系统中,当发生您已禁用 Activity
重建的配置变更时,Activity 会收到对 Activity.onConfigurationChanged()
的调用。任何附加的视图也会收到对 View.onConfigurationChanged()
的调用。对于您未添加到 android:configChanges
的配置变更,系统会照常重建 Activity。
onConfigurationChanged()
回调方法会接收一个 Configuration
对象,该对象指定了新的设备配置。读取 Configuration
对象中的字段以确定您的新配置。要进行后续更改,请更新您在界面中使用的资源。当系统调用此方法时,您的 Activity 的 Resources
对象会更新以返回基于新配置的资源。这使您无需系统重启您的 Activity 即可重置 UI 元素。
例如,以下 onConfigurationChanged()
实现检查键盘是否可用
Kotlin
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Checks whether a keyboard is available
if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_YES) {
Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show()
} else if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_NO) {
Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show()
}
}
Java
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Checks whether a keyboard is available
if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) {
Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show();
} else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO){
Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show();
}
}
如果您不需要根据这些配置变更更新您的应用,您可以选择不实现 onConfigurationChanged()
。在这种情况下,配置变更之前使用的所有资源仍将继续使用,并且您只是避免了 Activity 的重启。例如,电视应用可能不想在蓝牙键盘连接或断开时做出反应。
保留状态
当您使用此技术时,您仍必须在正常的 Activity 生命周期中保留状态。原因如下
- 不可避免的变更:您无法阻止的配置变更可能会重启您的应用。
- 进程终止:您的应用必须能够处理系统发起的进程终止。如果用户离开您的应用并将应用转到后台,系统可能会销毁该应用。
在 Jetpack Compose 中响应配置变更
Jetpack Compose 让您的应用更容易响应配置变更。但是,如果您禁用了所有可能禁用 Activity
重建的配置变更,您的应用仍然必须正确处理配置变更。
在 Compose UI 层次结构中,Configuration
对象可通过 LocalConfiguration
组合局部可用。每当它改变时,从 LocalConfiguration.current
读取的 composable 函数会重新组合。有关组合局部如何工作的更多信息,请参阅使用 CompositionLocal 的局部作用域数据。
示例
在以下示例中,一个可组合项以特定格式显示日期。该可组合项通过调用 ConfigurationCompat.getLocales()
并传入 LocalConfiguration.current
来响应系统区域设置配置变更。
@Composable
fun DateText(year: Int, dayOfYear: Int) {
val dateTimeFormatter = DateTimeFormatter.ofPattern(
"MMM dd",
ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
)
Text(
dateTimeFormatter.format(LocalDate.ofYearDay(year, dayOfYear))
)
}
为避免在区域设置变更时重建 Activity
,承载 Compose 代码的 Activity
需要选择退出区域设置配置变更。为此,您将 android:configChanges
设置为 locale|layoutDirection
。
配置变更:关键概念和最佳实践
以下是处理配置变更时需要了解的关键概念
- 配置:设备配置定义了 UI 如何向用户显示,例如应用显示尺寸、区域设置或系统主题。
- 配置变更:配置通过用户互动而改变。例如,用户可能更改设备设置或他们与设备的物理互动方式。无法阻止配置变更。
Activity
重建:配置变更默认会导致Activity
重建。这是一种内置机制,用于为新配置重新初始化应用状态。Activity
销毁:Activity
重建会导致系统销毁旧的Activity
实例,并在其位置创建一个新实例。旧实例现在已过时。任何对它的剩余引用都会导致内存泄漏、错误或崩溃。- 状态:旧
Activity
实例中的状态不会出现在新的Activity
实例中,因为它们是两个不同的对象实例。按照保存 UI 状态中所述,保留应用和用户状态。 - 选择退出:选择退出某种配置变更的 Activity 重建是一种潜在的优化。它要求您的应用能够根据新配置正确更新。
为了提供良好的用户体验,请遵循以下最佳实践
- 为频繁的配置变更做好准备:无论 API 级别、外形设备或 UI 工具包如何,都不要假设配置变更很少或从不发生。当用户导致配置变更时,他们期望应用能够更新并继续在新配置下正常工作。
- 保留状态:当
Activity
重建发生时,不要丢失用户状态。按照保存 UI 状态中所述,保留状态。 - 避免将选择退出作为快速修复:不要将选择退出
Activity
重建作为避免状态丢失的捷径。选择退出 Activity 重建要求您兑现处理变更的承诺,并且您仍然可能由于其他配置变更、进程终止或关闭应用导致的Activity
重建而丢失状态。完全禁用Activity
重建是不可能的。按照保存 UI 状态中所述,保留状态。 - 不要避免配置变更:不要对方向、宽高比或可调整大小施加限制,以避免配置变更和
Activity
重建。这会对希望以其首选方式使用您的应用的用户产生负面影响。
处理基于大小的配置变更
基于大小的配置变更可能随时发生,并且当您的应用在用户可以进入多窗口模式的大屏设备上运行时,更可能发生。用户期望您的应用在该环境中运行良好。
大小变更通常有两种类型:显著变更和不显著变更。显著大小变更是指由于屏幕尺寸(如宽度、高度或最小宽度)差异,新的配置会应用不同的备用资源集。这些资源包括应用自身定义的资源以及其任何库中的资源。
限制基于大小的配置变更的 Activity 重建
当您禁用基于大小的配置变更的 Activity
重建时,系统不会重建 Activity
。相反,它会收到对 Activity.onConfigurationChanged()
的调用。任何附加的视图也会收到对 View.onConfigurationChanged()
的调用。
当您的清单文件中包含 android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout"
时,基于大小的配置变更将禁用 Activity
重建。
允许基于大小的配置变更的 Activity 重建
在 Android 7.0 (API 级别 24) 及更高版本中,Activity
重建仅在大小变更显著时才会针对基于大小的配置变更发生。当系统由于大小不足而不重建 Activity
时,系统可能会转而调用 Activity.onConfigurationChanged()
和 View.onConfigurationChanged()
。
当 Activity
未重建时,关于 Activity
和 View
回调有一些注意事项
- 在 Android 11 (API 级别 30) 到 Android 13 (API 级别 33) 中,不会调用
Activity.onConfigurationChanged()
。 - 存在一个已知问题:在 Android 12L (API 级别 32) 和 Android 13 (API 级别 33) 的早期版本中,
View.onConfigurationChanged()
在某些情况下可能不会被调用。有关更多信息,请参阅此公共问题。此问题已在后续的 Android 13 版本和 Android 14 中得到解决。
对于依赖于监听基于大小的配置变更的代码,我们建议使用带有被覆盖的 View.onConfigurationChanged()
的实用程序 View
,而不是依赖于 Activity
重建或 Activity.onConfigurationChanged()
。