本文了解一下应用运行时当配置发生变化时,系统的应对机制以及应用的处理机制。

某些设备配置可能会在运行时更改(例如屏幕方向、键盘可用性和语言)。当发生这种变化时,Android 会重新启动正在运行的 ActivityonDestroy() 被调用,然后是 onCreate())。重新启动行为旨在通过使用与新设备配置匹配的备用资源自动重新加载应用,帮助您的应用适应新的配置。

要正确处理重新启动,重要的是您的 activity 通过正常的 Activity 生命周期恢复其以前的状态,在该生命周期中 Android 会在销毁 activity 之前调用 onSaveInstanceState(),以便保存有关应用状态的数据。然后可以在 onCreate()onRestoreInstanceState() 期间恢复状态。

有关如何使用 onSaveInstanceState() 的详细信息,请参见保存和恢复 activity 状态

为了测试应用是否随应用状态完好重启,您应该在应用中执行各种任务时调用配置更改(例如更改屏幕方向)。您的应用应该能够在任何时候重新启动,而不会丢失用户数据或状态,以便处理事件,例如配置更改或用户收到来电时,然后在应用进程销毁之后稍晚些时候返回到您的应用。要了解如何恢复 activity 状态,请阅读 Activity 生命周期

但是,您可能会遇到这种情况:重新启动应用和恢复大量数据可能代价高昂,并且会导致糟糕的用户体验。在这种情况下,你有两个选择:

a. 在配置更改期间保留一个对象

允许您的 activity 在配置更改时重新启动,但会将有状态的对象带入 activity 的新实例。

b. 自己处理配置更改

防止系统在某些配置更改期间重新启动您的 activity,但在配置发生更改时接受一个回调,以便您可以根据需要手动更新 activity。

Retaining an Object During a Configuration Change

如果重新启动 activity 需要恢复大量数据、重新建立网络连接或执行其他密集型操作,则由于配置更改而导致完全重新启动可能会降低用户体验。另外,您可能无法使用 Bundle 完全恢复您的 activity 状态,系统会使用 onSaveInstanceState() 回调为您保存 activity 状态 - 它不是设计用于承载大对象(如位图),并且其中的数据必须被序列化然后反序列化,这会消耗大量内存并使配置更改缓慢。在这种情况下,当您的 activity 由于配置更改而重新启动时,您可以通过保留 Fragment 来减轻重新初始化部分 activity 的负担。该 fragment 可以包含对要保留的有状态对象的引用。

当 Android 系统由于配置更改而关闭 activity 时,您标记为保留的 activity fragment 不会被销毁。您可以将这些 fragment 添加到您的 activity 中以保留有状态的对象。

在运行时配置更改期间保留 fragment 中的有状态对象:

  1. 扩展 Fragment 类并声明对有状态对象的引用。
  2. 创建 fragment 时调用 setRetainInstance(boolean)
  3. 将 fragment 添加到您的 activity 中。
  4. 使用 FragmentManager 在 activity 重新启动时检索 fragment。

例如,定义你的 fragment 如下:

public class RetainedFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

虽然 onCreate() 仅在首次创建保留 fragment 时调用一次,但您可以使用 onAttach()onActivityCreated() 知道保持 activity (the holding activity)何时准备好与此 fragment 交互。

并且在您的 activity 中,您可以使用此 fragment 在配置更改重新启动时保留状态。

然后使用 FragmentManager 将 fragment 添加到 activity 中。 在运行时配置更改期间,当 activity 再次启动时,您可以从 fragment 中获取数据对象。 例如,定义您的 activity 如下:

public class MyActivity extends Activity {

    private static final String TAG_RETAINED_FRAGMENT = "RetainedFragment";

    private RetainedFragment mRetainedFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        mRetainedFragment = (RetainedFragment) fm.findFragmentByTag(TAG_RETAINED_FRAGMENT);

        // create the fragment and data the first time
        if (mRetainedFragment == null) {
            // add the fragment
            mRetainedFragment = new RetainedFragment();
            fm.beginTransaction().add(mRetainedFragment, TAG_RETAINED_FRAGMENT).commit();
            // load data from a data source or perform any calculation
            mRetainedFragment.setData(loadMyData());
        }

        // the data is available in mRetainedFragment.getData() even after 
        // subsequent configuration change restarts.
        ...
    }
}

在这个例子中,onCreate() 添加一个 fragment 或者恢复对它的引用。 onCreate() 还将有状态对象存储在 fragment 实例中。

为了在不再需要的时候主动删除保留的 fragment,你可以在activity 的 onPause() 中检查 isFinishing()

    @Override
    public void onPause() {
        // perform other onPause related actions
        ...
        // this means that this activity will not be recreated now, user is leaving it
        // or the activity is otherwise finishing
        if(isFinishing()) {
            FragmentManager fm = getFragmentManager();
            // we will not need this fragment anymore, this may also be a good place to signal
            // to the retained fragment object to perform its own cleanup.
            fm.beginTransaction().remove(mDataFragment).commit();
        }
    }

警告:虽然可以存储任何对象,但不应传递与该 Activity 绑定的对象,例如 DrawableAdapterView 或与 Context 关联的任何其他对象。 如果这样做,它会泄漏原始 activity 实例的所有视图和资源。 (泄漏资源意味着你的应用保持对它们的保留,并且它们不能被垃圾收集,所以很多内存可能会丢失。)

Handling the Configuration Change Yourself

如果您的应用在特定的配置更改期间不需要更新资源,并且您的性能限制要求您避免重新启动 activity,则可以声明您的 activity 处理配置更改本身,从而防止系统重新启动 activity。

注意:自行处理配置更改可能会使其使用替代资源更加困难,因为系统不会自动为您应用它们。当您必须避免由于配置更改而重新启动时,应将此技术视为最后一招,并且不推荐将此技术用于大多数应用。

要声明您的 activity 处理配置更改,请在清单文件中编辑适当的 <activity> 元素,以包含 android:configChanges 属性的值,该值代表您要处理的配置。 android:configChanges 属性的文档中列出了可能的值(最常用的值是「orientation」以防止屏幕方向更改时重新启动,以及「keyboardHidden」以防止键盘可用性更改时重新启动)。您可以通过使用管道(|)字符分隔它们来在属性中声明多个配置值。

例如,以下清单代码声明了一个处理屏幕方向更改和键盘可用性更改的 activity:

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

现在,当其中一个配置发生更改时,MyActivity 不会重新启动。相反,MyActivity 接收到对 onConfigurationChanged() 的调用。此方法传递一个 Configuration 对象,该对象指定新的设备配置。通过阅读 Configuration 中的字段,您可以确定新的配置并通过更新界面中使用的资源进行适当的更改。在调用此方法时,您的 activity 的 Resources 对象将更新为基于新配置返回资源,因此您可以轻松地重置 UI 的元素,而无需系统重新启动活动。

警告:从 Android 3.2(API 级别 13)开始,当设备在纵向和横向之间切换时,「屏幕大小 - screen size」也会发生变化。因此,如果要在开发 API 级别 13 或更高级别时(由 minSdkVersiontargetSdkVersion 属性声明)防止由于方向更改导致运行时重新启动,除「orientation」值之外,还必须包含「screenSize」值。也就是说,你必须声明 android:configChanges=“orientation|screenSize”。但是,如果您的应用的目标级别为 12 或更低,则您的 activity 始终会自行处理此配置更改(即使在 Android 3.2 或更高版本的设备上运行,此配置更改也不会重新启动您的 activity)。

例如,以下 onConfigurationChanged() 实现检查当前的设备方向:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

Configuration 对象表示所有当前配置,而不仅仅是已经改变的配置。大多数情况下,您不会在意配置如何更改,只需重新分配由您正在处理的配置提供的替代方案的所有资源即可。例如,由于 Resources 对象现在已更新,因此可以使用 setImageResource() 重置任何 ImageView,并使用新配置的相应资源。

请注意,Configuration 字段中的值是与来自 Configuration 类的特定常量相匹配的整数。有关哪些常量用于每个字段的文档,请参阅 Configuration 参考中的相应字段。

请记住:当您声明您的 activity 来处理配置更改时,您有责任重置您提供替代方案的任何元素。如果声明 activity 来处理方向更改并使图像在横向和纵向之间更改,则必须在 onConfigurationChanged() 过程中将每个资源重新分配给每个元素。

如果您不需要根据这些配置更改更新您的应用程序,则可以不实现 onConfigurationChanged()。在这种情况下,配置更改之前使用的所有资源仍将被使用,并且您只能避免重新启动 activity。但是,您的应用应始终能够关闭并重新启动其先前的状态,因此在正常的 activity 生命周期中,您不应该认为此技术可以避免保留您的状态。不仅因为其他配置更改无法阻止重新启动应用,而且还因为您应该处理事件,例如用户离开应用时它会在用户返回之前被销毁。

有关您可以在活动中处理哪些配置更改的更多信息,请参阅 android:configChanges 文档和 Configuration 类。

References

  1. Handling Configuration Changes