处理配置变更

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

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

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

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

activity 重新创建

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

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

重新创建示例

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

重新创建过程还会清除您在 Activity 或其包含的 FragmentView 或其他对象中,以字段形式保留的任何状态。这是因为 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 重启。您无法将其停用。例如,您无法停用 Android 12L(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 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。例如,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() 的调用。

如果您的清单文件中包含 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 与被替换的 View.onConfigurationChanged() 搭配使用,而不是依赖于 Activity 重新创建或 Activity.onConfigurationChanged()