处理配置变更

某些设备配置可在应用运行时发生变化。其中包括但不限于

  • 应用显示尺寸
  • 屏幕方向
  • 字体大小和字重
  • 区域设置
  • 深色模式与浅色模式
  • 键盘可用性

大多数这些配置变更都是由用户互动引起的。例如,旋转或折叠设备会改变应用可用的屏幕空间量。同样,更改设备设置(例如字体大小、语言或首选主题)也会改变 Configuration 对象中对应的相应值。

这些参数通常要求对应用界面进行足够大的更改,以至于 Android 平台为此类更改提供了专门构建的机制。此机制是 Activity 重建

Activity 重建

当发生配置变更时,系统会重建 Activity。为此,系统会调用 onDestroy() 并销毁现有的 Activity 实例。然后,它会使用 onCreate() 创建一个新实例,并且此新的 Activity 实例会使用新的、更新的配置进行初始化。这也意味着系统也会使用新配置重建 UI。

重建行为有助于您的应用适应新配置,方法是自动使用与新设备配置匹配的备用资源重新加载您的应用。

重建示例

考虑一个 TextView,它使用布局 XML 文件中定义的 android:text="@string/title" 显示一个静态标题。当视图创建时,它会根据当前语言精确设置文本一次。如果语言发生变化,系统会重建 Activity。因此,系统也会重建视图并根据新语言将其初始化为正确的值。

重建还会清除作为 Activity 或其包含的任何 FragmentView 或其他对象中的字段保留的任何状态。这是因为 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 未重建时,关于 ActivityView 回调有一些注意事项

  • 在 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()