处理配置变更

某些设备配置可能会在应用运行期间发生变更。这包括但不限于:

  • 应用显示大小
  • 屏幕方向
  • 字体大小和粗细
  • 语言区域
  • 深色模式与浅色模式
  • 键盘可用性

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

这些参数通常需要对应用界面进行充分的更改。因此,Android 平台提供了一种专有机制来处理这种更改。这种机制就是 activity 重新创建

activity 重新创建

当发生配置更改时,系统会重新创建 activity。系统会调用 onDestroy() 并销毁现有的 activity 实例。随后,系统会使用 onCreate() 创建一个新实例。这个新的 activity 实例会使用更新后的配置进行初始化。这也意味着,系统还会使用新配置重新创建界面。

重新创建行为会自动利用与新设备配置相匹配的备用资源来自动重新加载您的应用,从而帮助它适应新配置。

重新创建示例

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

重新创建还会清除您在 activity 或其包含的 fragment、视图和其他对象中的保留的任何状态字段。这是因为 activity 重新创建会创建全新的 activity 和界面实例。此外,之前的旧 activity 不再可见或不再有效,因此对该 activity 或其所含对象的任何其余引用都已过时。它们可能会导致 bug、内存泄漏和崩溃。

用户期望

应用用户希望保留状态。如果用户在填写表单时在多窗口模式下打开另一个应用来参考信息,却在返回表单时发现表单已清空,或者直接跳转至应用的其他位置,那么用户体验将会非常糟糕。您必须通过配置变更和 activity 重新创建来确保提供一致的体验。

如需验证应用中是否保留了状态,您可以在应用处于前台和后台时执行会导致配置变更的操作。上述操作包括:

  • 旋转设备
  • 进入多窗口模式
  • 在多窗口模式或自由窗口模式下调整应用大小
  • 折叠具有多个显示屏的可折叠设备
  • 更改系统主题,例如深色模式与浅色模式
  • 更改字体大小
  • 更改系统或应用语言
  • 连接或断开硬件键盘
  • 连接或断开基座

在通过重新创建 activity 来保留相关状态时,您可以采用三种主要方法。具体方法取决于您要保留的状态类型:

保存界面状态页面详细介绍了各个 API 以及各自适用的使用场景。

限制 activity 重新创建

您可以禁止在发生特定配置变更时自动重新创建 activity。activity 重新创建会导致重新创建整个界面以及从 activity 派生的所有对象。您有充分的理由禁止此行为。例如,您的应用可能不需要在特定的配置变更期间更新资源,或者您可能会受到性能限制。在这种情况下,您可以声明由 activity 来自行处理配置变更,并禁止系统重启 activity。

您可以为特定配置更改停用 activity 重新创建。为此,请将配置类型添加到 AndroidManifest.xml<activity> 条目中的 android:configChangesandroid:configChanges 属性文档中介绍了可能的值。

以下清单代码将在屏幕方向和键盘可用性发生更改时为 MyActivity 停用 activity 重新创建:

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
    android:label="@string/app_name">

某些配置变更始终会导致 activity 重启。您无法将其停用。例如,您无法停用 API 32 中引入的动态配色变更。未来,更多配置变更可能会采用类似的行为方式。

对 View 系统中的配置变更做出响应

在 View 系统中,如果发生配置变更并且您已停用 activity 重新创建,activity 会收到对 Activity.onConfigurationChanged() 的调用。任何关联的视图也会收到对 View.onConfigurationChanged() 的调用。对于尚未添加到 android:configChanges 的配置变更,系统会照常重新创建 activity。

onConfigurationChanged() 回调方法会收到一个 Configuration 对象,其中指定了新的设备配置。读取 Configuration 对象中的字段以确定合适的新配置。如需进行后续更改,请更新您在接口中使用的资源。当系统调用此方法时,activity 的 Resources 对象会相应地进行更新,并根据新配置返回资源。这样一来,您就可以在系统不重启 activity 的情况下轻松重置界面元素。

例如,以下 onConfigurationChanged() 实现会检查是否有可用的键盘:

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // Checks whether any 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 any 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。例如,TV 应用可能不希望在连接或断开蓝牙键盘时做出反应。

保留状态

使用此方法时,您仍然需要在正常的 activity 生命周期内保留状态。原因如下:

  • 不可避免的更改:您无法阻止的配置变更可能会导致您的应用重启。
  • 进程终止:您的应用应当能够处理系统发起的进程终止。如果用户离开您的应用,使其在后台运行,则系统可能会终止该应用。

在 Jetpack Compose 中响应配置变更

Jetpack Compose 可让您的应用更轻松地响应配置变更。不过,如果您尽可能为所有配置变更停用 activity 重新创建,则仍然需要确保应用能够正确处理配置变更。

可以在具有 LocalConfiguration CompositionLocal 的 Compose 界面层次结构中使用 Configuration 对象。每当发生变更时,从 LocalConfiguration.current 进行读取的可组合函数都会重组。如需详细了解 CompositionLocal 的运行方式,请参阅使用 CompositionLocal 将数据的作用域限定在本地文档。

示例

在以下示例中,可组合项显示具有特定格式的日期。该可组合项通过使用 LocalConfiguration.current 调用 ConfigurationCompat.getLocales() 来响应系统语言区域配置变更。

@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

配置变更关键概念和要点

总的来说,您在处理配置变更时需要注意以下几点:

  • 配置:设备配置定义界面应如何向用户显示内容,例如应用显示大小、语言区域或系统主题。
  • 配置变更:配置根据用户互动发生变更。例如,用户可能会更改设备设置或他们与设备的物理互动方式。配置会发生变更;无法“阻止”配置变更。
  • activity 重新创建:默认情况下,配置变更会导致重新创建 activity。这是为新配置重新初始化应用状态的内置机制。
  • activity 销毁:activity 重新创建会导致系统销毁旧的 activity 实例,并创建一个新实例来代替它。旧实例现已过时;对该实例的任何其余引用都会导致内存泄漏、bug 或崩溃。
  • 状态:旧 activity 实例中的状态不存在于新 activity 实例中,因为它们是两个不同的对象实例。请按照保存界面状态中描述的方法保留应用和用户状态。
  • 停用:为某种类型的配置变更停用 activity 重新创建是一种潜在的优化方案。您需要确保应用根据新配置进行正确更新。

最佳实践

为了提供良好的用户体验,请注意以下几点:

  • 配置会频繁发生变更:对于任何 API 级别、外形规格和界面工具包,都不要假设很少或永远不会发生配置变更。当用户导致配置变更时,他们希望应用进行更新,并继续使用新配置正常运行。
  • 保留状态:重新创建 activity 时不会丢失用户状态。请按照保存界面状态中描述的方法保留状态。
  • 避免停用快速修复功能:请勿停用 activity 重新创建,这样可以轻松避免丢失状态。停用 activity 重新创建需要实现处理变更的承诺,而且当其他配置变更、进程终止或应用关闭导致 activity 重新创建时,您仍然可能会丢失状态。无法完全停用 activity 重新创建。请按照保存界面状态中描述的方法保留状态。
  • 不要避免配置变更:请勿通过对屏幕方向、宽高比或可调整性设置限制来避免配置变更和 activity 重新创建。这会对希望以首选方式使用应用的用户产生负面影响。

处理基于大小的配置变更

基于大小的配置可能会随时发生变更。当您的应用在用户可以进入多窗口模式大屏幕设备上运行时,这种问题会更频繁地发生。他们希望您的应用可以在该环境中正常运行。

大小变更通常分为两类:显著变更和细微变更。“显著”大小变更是指由于屏幕尺寸不同(例如宽度、高度或最小宽度)而将一组不同的备用资源应用于新配置。这些资源包括应用自行定义的资源,以及其中包含资源的任何库。

限制为基于大小的配置变更重新创建 activity

为基于大小的配置变更停用 activity 重新创建后,系统不会重新创建 activity,而是会收到对 Activity.onConfigurationChanged() 的调用。任何关联的视图都会收到对 View.onConfigurationChanged() 的调用。

允许为基于大小的配置变更重新创建 activity

在 API 24 及更高级别上,只有当发生基于大小的配置变更时,才会发生 activity 重新创建。当系统因大小不足而未重新创建 activity 时,系统可能会调用 Activity.onConfigurationChanged()View.onConfigurationChanged()

关于 activity 和视图回调,您应当注意以下几点:

  • 在 API 30 及更高版本中,系统不会调用 Activity.onConfigurationChanged() 回调。
  • 在 API 32 和 API 33 上,不会调用 View.onConfigurationChanged()。后续的 API 版本对此 bug 进行了修复。

对于依赖于监听基于大小的配置变更的代码,建议使用具有被替换的 View.onConfigurationChanged() 的实用程序视图,而不是依赖于 activity 重新创建或 Activity.onConfigurationChanged()