处理配置更改

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

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

大多数这些配置更改是由于某些用户交互而发生的。例如,旋转或折叠设备会更改应用可用的屏幕空间量。同样,更改设备设置(如字体大小、语言或首选主题)会更改 Configuration 对象中各自的值。

这些参数通常需要对应用程序的 UI 进行足够大的更改,以至于 Android 平台具有一个专门用于更改发生时的机制。这种机制是Activity 重新创建

Activity 重新创建

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

重新创建行为通过自动使用与新设备配置匹配的替代资源重新加载应用程序来帮助您的应用程序适应新的配置。

重新创建示例

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

重新创建还会清除作为 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 属性的文档中。

以下清单代码在屏幕方向和键盘可用性发生变化时禁用MyActivityActivity 重新创建

<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 重新创建,则您的应用仍然必须正确处理配置更改。

Configuration 对象在 Compose UI 层次结构中使用LocalConfiguration 组合局部可用。每当它发生更改时,从LocalConfiguration.current 读取的可组合函数都会重新组合。有关组合局部如何工作的更多信息,请参阅使用 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

配置更改:关键概念和最佳实践

以下是在处理配置更改时需要了解的关键概念

  • 配置:设备配置定义了 UI 如何显示给用户,例如应用显示大小、区域设置或系统主题。
  • 配置更改:配置通过用户交互更改。例如,用户可能会更改设备设置或他们与设备的物理交互方式。无法阻止配置更改。
  • Activity 重新创建:配置更改默认会导致Activity 重新创建。这是一种内置机制,用于为新配置重新初始化应用状态。
  • Activity 销毁:Activity 重新创建会导致系统销毁旧的Activity 实例并在其位置创建一个新的实例。旧实例现在已过时。对它的任何剩余引用都会导致内存泄漏、错误或崩溃。
  • 状态:Activity 实例中的状态在新Activity 实例中不存在,因为它们是两个不同的对象实例。请按照保存 UI 状态 中的说明保留应用和用户的状态。

  • 退出:对于某种配置更改类型,退出活动重新创建是一种潜在的优化方法。它要求您的应用能够正确地响应新的配置进行更新。

为了提供良好的用户体验,请遵循以下最佳实践

  • 做好应对频繁配置更改的准备:无论 API 级别、外形尺寸或 UI 工具包如何,都不要假设配置更改很少发生或从不发生。当用户导致配置更改时,他们期望应用能够更新并继续使用新的配置正常工作。
  • 保留状态:Activity重新创建发生时,不要丢失用户的状态。请按照保存 UI 状态中所述保留状态。
  • 避免将退出作为快速修复:不要将退出Activity重新创建作为避免状态丢失的捷径。退出活动重新创建要求您履行处理更改的承诺,并且由于来自其他配置更改、进程死亡或关闭应用的Activity重新创建,您仍然可能丢失状态。无法完全禁用Activity重新创建。请按照保存 UI 状态中所述保留状态。
  • 不要避免配置更改:不要对方向、纵横比或可调整大小性设置限制以避免配置更改和Activity重新创建。这会对希望以其首选方式使用您的应用的用户产生负面影响。

处理基于大小的配置更改

基于大小的配置更改可能随时发生,并且当您的应用在用户可以进入多窗口模式大屏幕设备上运行时,这种情况发生的可能性更大。他们期望您的应用在该环境中运行良好。

一般来说,大小更改分为两种类型:显著和不显著。显著的大小更改是指由于屏幕尺寸(如宽度、高度或最小宽度)的不同,导致一组不同的替代资源应用于新配置的大小更改。这些资源包括应用本身定义的资源以及来自其任何库的资源。

限制基于大小的配置更改的活动重新创建

当您禁用基于大小的配置更改的Activity重新创建时,系统不会重新创建Activity。而是会调用Activity.onConfigurationChanged()。任何附加的视图都会收到对View.onConfigurationChanged()的调用。

当您在清单文件中包含android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout"时,将为基于大小的配置更改禁用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()