活动生命周期

活动是 Android 应用程序的构建基块,它们能够以多种不同的状态存在。 活动生命周期从实例化开始,以销毁结束,包括中间的许多状态。 当活动更改状态时,将调用相应的生命周期事件方法,通知活动即将发生的状态更改,并允许其执行代码来适应这种更改。 本文探讨活动的生命周期,并解释活动在每个状态更改期间所承担的责任,以使应用程序行为正常且可靠。

活动生命周期概述

活动是 Android 特有的一个不常见的编程概念。 在传统的应用程序开发中,通常会执行一个静态 main 方法以启动应用程序。 但对于 Android,情况有所不同;可以通过应用程序内的任何已注册活动启动 Android 应用程序。 实际上,大多数应用程序将只有一个被指定为应用程序入口点的特定活动。 但是,如果应用程序崩溃或由 OS 终止,则 OS 可以尝试在上一次打开的活动中或上一个活动堆栈中的任何其他位置重新启动应用程序。 此外,当 OS 不处于活动状态时,可能会暂停活动,当内存不足时,则回收活动。 若要使应用程序能够在活动重启时正确地还原其状态,必须谨慎考虑,尤其是在该活动依赖于先前活动中的数据时。

活动生命周期作为 OS 在活动的整个生命周期中调用的方法的集合来实现。 这些方法允许开发人员实现满足其应用程序的状态和资源管理要求所需的功能。

对于应用程序开发人员而言,分析每个活动的需求以确定需要实现活动生命周期所公开的方法极其重要。 如果不这样做,可能会导致应用程序不稳定、崩溃、资源膨胀,甚至可能导致基础 OS 不稳定。

本章详细介绍活动生命周期,包括:

  • 活动状态
  • 生命周期方法
  • 保留应用程序的状态

本部分还包括一个演练,其中提供了有关如何在活动生命周期内有效保存状态的实用示例。 本章结束时,你应了解活动生命周期,以及如何在 Android 应用程序中为其进行支持。

活动生命周期

Android 活动生命周期包含一系列在“活动”类中公开的方法,这些方法为开发人员提供了资源管理框架。 此框架使开发人员能够满足应用程序内每个活动的独特状态管理要求,并正确处理资源管理。

活动状态

Android OS 仲裁活动基于其状态。 这有助于 Android 识别不再使用的活动,从而允许 OS 回收内存和资源。 下图说明了活动在其生存期内可以经历的状态:

Activity states diagram

这些状态可分为 4 个主要组,如下所示:

  1. 活动或正在运行 – 如果活动位于前台(也称为活动堆栈的顶部),则其状态视为活动或正在运行。 这被视为 Android 中优先级最高的活动,因此只有在极端情况下才会被 OS 终止,例如,如果该活动尝试使用比设备上可用的内存更多的内存,则可能会导致 UI 无响应。

  2. 已暂停 – 当设备进入睡眠状态,或者某个活动仍然可见但被新的、非完整或透明的活动部分隐藏时,该活动将被视为已暂停。 已暂停的活动仍处于活动状态,即,它们维护所有状态和成员信息,并保持附加到窗口管理器。 这被视为 Android 中优先级第二高的活动,因此,仅当终止此活动能够满足保持活动/正在运行的活动稳定和响应所需的资源需求时,OS 才会终止该活动。

  3. 已停止/后台 – 被另一个活动完全遮挡的活动被视为已停止或处于后台。 已停止的活动仍会尝试尽可能地保留其状态和成员信息,但已停止的活动被视为这三种状态中的优先级最低,因此,OS 将先终止此状态的活动,以满足较高优先级活动的资源需求。

  4. 已重启 – Android 可能会从内存中删除处于生命周期中从已暂停到已停止的任何状态的活动。 如果用户导航回该活动,则必须重新启动该活动,还原到之前保存的状态,然后向用户显示。

重新创建活动以响应配置更改

让问题变得更加复杂的是,Android 又增加了一个麻烦,称为配置更改。 配置更改是快速的活动销毁/重新创建周期,当活动的配置发生更改时发生,例如当设备旋转时(并且活动需要以横向或纵向模式重新构建)、显示键盘时(并且活动有机会调整自身大小),或者当设备放置在坞站中时等。

配置更改仍会导致与停止和重启活动期间发生的相同的活动状态更改。 然而,为了确保应用程序在配置更改期间具有良好的响应性和性能,尽快处理这些更改非常重要。 因此,Android 有一个特定的 API,可用于在配置更改期间保留状态。 稍后将在管理整个生命周期的状态部分中介绍这一点。

活动生命周期方法

Android SDK 以及扩展的 Xamarin.Android 框架提供了一个用于管理应用程序内活动状态的强大模型。 当活动的状态发生变化时,OS 会通知该活动,这会对该活动调用特定的方法。 下图演示了与活动生命周期相关的这些方法:

Activity Lifecycle flowchart

作为开发人员,你可以通过在活动中重写这些方法来处理状态更改。 但是,请务必注意,所有生命周期方法都在 UI 线程上调用,并将阻止 OS 执行下一个 UI 工作,例如隐藏当前活动、显示新活动等。因此,这些方法中的代码应尽可能简短,以使应用程序有效运行。 任何长期运行的任务都应该在后台线程上执行。

让我们了解一下这些生命周期方法及其用途:

OnCreate

OnCreate 是创建活动时要调用的第一种方法。 OnCreate 始终被替代以执行活动可能需要的任何启动初始化,例如:

  • 正在创建视图
  • 初始化变量
  • 将静态数据绑定到列表

OnCreate 采用捆绑包参数,该参数是用于在活动之间存储和传递状态信息和对象的字典。如果捆绑包不为 null,则表明活动正在重启,并且应该从上一个实例还原其状态。 以下代码演示如何从捆绑包中检索值:

protected override void OnCreate(Bundle bundle)
{
   base.OnCreate(bundle);

   string intentString;
   bool intentBool;

   if (bundle != null)
   {
      intentString = bundle.GetString("myString");
      intentBool = bundle.GetBoolean("myBool");
   }

   // Set our view from the "main" layout resource
   SetContentView(Resource.Layout.Main);
}

OnCreate 完成后,Android 将调用 OnStart

OnStart

完成后,系统始终调用 OnStartOnCreate。 如果活动需要在某活动可见之前执行任何特定的任务,例如刷新活动中视图的当前值,则活动可能会替代此方法。 Android 将在此方法后立即调用 OnResume

OnResume

当活动准备好开始与用户交互时,系统会调用 OnResume。 活动应替代此方法以执行如下任务:

  • 提高帧速率(游戏开发中的常见任务)
  • 开始动画
  • 侦听 GPS 更新
  • 显示任何相关的警报或对话框
  • 连接外部事件处理程序

例如,以下代码段演示如何初始化照相机:

protected override void OnResume()
{
    base.OnResume(); // Always call the superclass first.

    if (_camera==null)
    {
        // Do camera initializations here
    }
}

OnResume 很重要,因为在 OnPause 中完成的任何操作都应该在 OnResume 中取消完成,它是保证在 OnPause 之后使活动重新恢复时执行的唯一生命周期方法。

OnPause

当系统即将将活动置于后台或活动部分被遮挡时,将调用 OnPause。 如果活动需要,则应替代此方法:

  • 将未保存的更改提交到持久数据

  • 销毁或清理其他消耗资源的对象

  • 降低帧速率和暂停动画

  • 取消注册外部事件处理程序或通知处理程序(即与服务绑定的处理程序)。 必须执行此操作以防止活动内存泄露。

  • 同样,如果活动已显示任何对话框或警报,则必须使用 .Dismiss() 方法清除。

例如,以下代码段将释放相机,因为活动无法在暂停时使用它:

protected override void OnPause()
{
    base.OnPause(); // Always call the superclass first

    // Release the camera as other activities might need it
    if (_camera != null)
    {
        _camera.Release();
        _camera = null;
    }
}

OnPause 后将调用两种可能的生命周期方法:

  1. 如果要将活动返回到前台,则调用 OnResume
  2. 如果将活动置于后台,则调用 OnStop

OnStop

当不再向用户显示活动时,将调用 OnStop。 发生下列情况之一时,会发生此行为:

  • 一个新活动正在启动,将覆盖此活动。
  • 现有活动将带到前台。
  • 正在销毁活动。

OnStop 可能并不总是在内存不足的情况下被调用,例如当 Android 资源不足并且无法正确将活动置于后台时。 因此,在准备销毁活动时,最好不要依赖 OnStop 的调用。 如果活动即将消失,在此之后可能调用的下一个生命周期方法将是 OnDestroy,如果活动返回与用户交互,则可能调用 OnRestart

OnDestroy

OnDestroy 是在活动实例被销毁并完全从内存中删除之前对活动实例调用的最后一个方法。 在极端情况下,Android 可能会终止托管活动的应用程序进程,这将导致无法调用 OnDestroy。 大多数活动不会实现此方法,因为大部分清理和关闭都是在 OnPauseOnStop 方法中完成的。 通常会替代 OnDestroy 方法来清理可能泄漏资源的长期运行的任务。 例如,在 OnCreate 中启动的后台线程。

销毁活动后,将不会调用任何生命周期方法。

OnRestart

OnRestart 在活动停止后、再次启动之前调用。 一个很好的例子是,当用户在应用程序中执行活动时按下“主页”按钮。 发生这种情况时,将调用 OnPause 方法,然后调用 OnStop 方法,并且活动会移至后台但不会被销毁。 如果用户随后使用任务管理器或类似的应用程序还原应用程序,Android 将调用该活动的 OnRestart 方法。

对于在 OnRestart 中应实现何种逻辑,没有通用准则。 这是因为无论活动是否正在创建或重启,都始终调用 OnStart,因此活动所需的任何资源都应该在 OnStart 中初始化,而不是 OnRestart 中。

下一个在 OnRestart 之后调用的生命周期方法将是 OnStart

“后退”与Home

许多 Android 设备都有两个不同的按钮:“后退”按钮和“主页”按钮。 在以下 Android 4.0.3 屏幕截图中可以看到这样的示例:

Back and Home buttons

这两个按钮之间存在细微差异,尽管它们看起来具有将应用程序置于后台的相同效果。 当用户单击“后退”按钮时,即告知 Android 已经完成了活动。 Android 将销毁活动。 相反,当用户单击“主页”按钮时,活动仅放置在后台 - Android 不会终止活动。

管理整个生命周期的状态

当活动停止或销毁时,系统将提供保存活动状态供以后解除冻结的机会。 这种已保存的状态称为实例状态。 Android 提供了三个选项,用于存储活动生命周期中的实例状态:

  1. 将基元值存储存储在称为捆绑包Dictionary 中,Android 将使用它来保存状态。

  2. 创建一个自定义类来保存复杂的值,例如位图。 Android 将使用此自定义类保存状态。

  3. 绕过配置更改生命周期,承担维护活动中状态的完整责任。

本指南介绍前两个选项。

捆绑包状态

保存实例状态的第一个选项是使用称为捆绑包的键/值字典对象。 回想一下,当创建活动时,OnCreate 方法会传递一个捆绑包作为参数,该捆绑包可用于还原实例状态。 不建议对无法快速或轻松序列化为键/值对(例如位图)的更复杂数据使用捆绑包;相反,它应该用于简单的值,例如字符串。

活动提供有助于保存和检索捆绑包中实例状态的方法:

  • OnSaveInstanceState – 当销毁活动时由 Android 调用。 如果活动需要保留任何键/值状态项,可以实施此方法。

  • OnRestoreInstanceState – 在 OnCreate 方法完成后调用此方法,并为活动在初始化完成后还原其状态提供了另一个机会。

下图说明了如何使用这些方法:

Bundle states flowchart

OnSaveInstanceState

OnSaveInstanceState 将在活动停止时调用。 它将接收一个捆绑包参数,活动可以在其中存储其状态。 当设备遇到配置更改时,活动可以使用传入的 Bundle 对象来通过替代 OnSaveInstanceState 保留活动状态。 例如,考虑以下代码:

int c;

protected override void OnCreate (Bundle bundle)
{
  base.OnCreate (bundle);

  this.SetContentView (Resource.Layout.SimpleStateView);

  var output = this.FindViewById<TextView> (Resource.Id.outputText);

  if (bundle != null) {
    c = bundle.GetInt ("counter", -1);
  } else {
    c = -1;
  }

  output.Text = c.ToString ();

  var incrementCounter = this.FindViewById<Button> (Resource.Id.incrementCounter);

  incrementCounter.Click += (s,e) => {
    output.Text = (++c).ToString();
  };
}

当单击名为 incrementCounter 的按钮时,上面的代码会递增名为 c 的整数,并在名为 outputTextView 中显示结果。 发生配置更改时(例如,当设备旋转时)时,上述代码将丢失 c 的值,因为 bundle 将为 null,如下图所示:

Display does not show previous value

若要保留此示例中 c 的值,活动可以替代 OnSaveInstanceState,将值保存在捆绑包中,如下所示:

protected override void OnSaveInstanceState (Bundle outState)
{
  outState.PutInt ("counter", c);
  base.OnSaveInstanceState (outState);
}

现在,当设备旋转到新方向时,整数将保存在捆绑包中,并按行进行检索:

c = bundle.GetInt ("counter", -1);

注意

请务必始终调用 OnSaveInstanceState 的基本实现,以便还可以保存视图层次结构的状态。

视图状态

替代 OnSaveInstanceState 是一种适当的机制,用于在不同方向的更改中保存活动中的临时数据,如上面示例中的计数器。 不过,默认 OnSaveInstanceState 实现将负责为每个视图在 UI 中保存临时数据,只要每个视图分配有 ID。 例如,假设应用程序在 XML 中定义了 EditText 元素,如下所示:

<EditText android:id="@+id/myText"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"/>

由于 EditText 控件已分配有 id,因此当用户输入某些数据并旋转设备时,仍会显示数据,如下所示:

Data is preserved in landscape mode

OnRestoreInstanceState

OnRestoreInstanceState 将在 OnStart 之后调用。 它为活动提供了还原先前 OnSaveInstanceState 期间保存到捆绑包的任何状态的机会。 然而,这与提供给 OnCreate 的捆绑包相同。

以下代码演示如何还原 OnRestoreInstanceState 中的状态:

protected override void OnRestoreInstanceState(Bundle savedState)
{
    base.OnRestoreInstanceState(savedState);
    var myString = savedState.GetString("myString");
    var myBool = savedState.GetBoolean("myBool");
}

此方法的存在是为了在还原状态的时间方面提供一些灵活性。 有时,等到所有初始化完成后再还原实例状态更为合适。 此外,现有活动的子类可能只想从实例状态还原某些值。 在许多情况下,不需要替代 OnRestoreInstanceState,因为大多数活动都可以使用提供给 OnCreate 的捆绑包来还原状态。

有关使用 Bundle 保存状态的示例,请参阅演练 - 保存活动状态

绑定限制

尽管 OnSaveInstanceState 可以轻松保存临时数据,但存在一些限制:

  • 并非所有情况下都调用它。 例如,按“主页”或“后退”退出活动将导致调用 OnSaveInstanceState

  • 传入 OnSaveInstanceState 的捆绑包不是针对大型对象(如图像)设计的。 对于大型对象,最好从 OnRetainNonConfigurationInstance 保存对象,如下所示。

  • 使用捆绑包保存的数据会进行序列化,这可能会导致延迟。

捆绑状态对于不使用太多内存的简单数据非常有用,而非配置实例数据对于更复杂的数据或检索成本高昂的数据(例如从 Web 服务调用或复杂的数据库查询)非常有用。 非配置实例数据根据需要保存在对象中。 下一部分将介绍 OnRetainNonConfigurationInstance 作为通过配置更改保留更复杂数据类型的一种方法。

保留复杂数据

除了在捆绑包中保存数据外,Android 还支持通过替代 OnRetainNonConfigurationInstance 并返回包含要保存的数据的 Java.Lang.Object 实例来保存数据。 使用 OnRetainNonConfigurationInstance 保存状态有两个主要好处:

  • OnRetainNonConfigurationInstance 返回的对象适用于更大、更复杂的数据类型,因为内存保留了该对象。

  • OnRetainNonConfigurationInstance 方法是按需调用的,并且仅在需要时调用。 这比使用手动缓存更经济。

使用 OnRetainNonConfigurationInstance 适合多次检索数据成本高昂的场景,例如在 Web 服务调用中。 例如,请考虑以下搜索 Twitter 的代码:

public class NonConfigInstanceActivity : ListActivity
{
  protected override void OnCreate (Bundle bundle)
  {
    base.OnCreate (bundle);
    SearchTwitter ("xamarin");
  }

  public void SearchTwitter (string text)
  {
    string searchUrl = String.Format("http://search.twitter.com/search.json?" + "q={0}&rpp=10&include_entities=false&" + "result_type=mixed", text);

    var httpReq = (HttpWebRequest)HttpWebRequest.Create (new Uri (searchUrl));
    httpReq.BeginGetResponse (new AsyncCallback (ResponseCallback), httpReq);
  }

  void ResponseCallback (IAsyncResult ar)
  {
    var httpReq = (HttpWebRequest)ar.AsyncState;

    using (var httpRes = (HttpWebResponse)httpReq.EndGetResponse (ar)) {
      ParseResults (httpRes);
    }
  }

  void ParseResults (HttpWebResponse httpRes)
  {
    var s = httpRes.GetResponseStream ();
    var j = (JsonObject)JsonObject.Load (s);

    var results = (from result in (JsonArray)j ["results"] let jResult = result as JsonObject select jResult ["text"].ToString ()).ToArray ();

    RunOnUiThread (() => {
      PopulateTweetList (results);
    });
  }

  void PopulateTweetList (string[] results)
  {
    ListAdapter = new ArrayAdapter<string> (this, Resource.Layout.ItemView, results);
  }
}

此代码从 Web 检索 JSON 格式的结果,解析结果,然后将结果显示在列表中,如以下屏幕截图所示:

Results displayed on screen

发生配置更改时(例如,当设备旋转时)时,代码会重复该过程。 若要重复使用最初检索的结果,并且不造成不必要的、冗余的网络调用,可以使用 OnRetainNonconfigurationInstance 来保存结果,如下所示:

public class NonConfigInstanceActivity : ListActivity
{
  TweetListWrapper _savedInstance;

  protected override void OnCreate (Bundle bundle)
  {
    base.OnCreate (bundle);

    var tweetsWrapper = LastNonConfigurationInstance as TweetListWrapper;

    if (tweetsWrapper != null) {
      PopulateTweetList (tweetsWrapper.Tweets);
    } else {
      SearchTwitter ("xamarin");
    }

    public override Java.Lang.Object OnRetainNonConfigurationInstance ()
    {
      base.OnRetainNonConfigurationInstance ();
      return _savedInstance;
    }

    ...

    void PopulateTweetList (string[] results)
    {
      ListAdapter = new ArrayAdapter<string> (this, Resource.Layout.ItemView, results);
      _savedInstance = new TweetListWrapper{Tweets=results};
    }
}

现在,当设备旋转时,将从 LastNonConfiguartionInstance 属性中检索最初结果。 在此示例中,结果由包含推文的 string[] 组成。 由于 OnRetainNonConfigurationInstance 需要返回一个 Java.Lang.Object,因此 string[] 包装在子类为 Java.Lang.Object 的类中,如下所示:

class TweetListWrapper : Java.Lang.Object
{
  public string[] Tweets { get; set; }
}

例如,尝试使用 TextView 作为从 OnRetainNonConfigurationInstance 返回的对象将泄漏活动,如以下代码所示:

TextView _textView;

protected override void OnCreate (Bundle bundle)
{
  base.OnCreate (bundle);

  var tv = LastNonConfigurationInstance as TextViewWrapper;

  if(tv != null) {
    _textView = tv;
    var parent = _textView.Parent as FrameLayout;
    parent.RemoveView(_textView);
  } else {
    _textView = new TextView (this);
    _textView.Text = "This will leak.";
  }

  SetContentView (_textView);
}

public override Java.Lang.Object OnRetainNonConfigurationInstance ()
{
  base.OnRetainNonConfigurationInstance ();
  return _textView;
}

在本部分中,我们学习了如何使用 Bundle 保存简单的状态数据,并使用 OnRetainNonConfigurationInstance 保存更复杂的数据类型。

总结

Android 活动生命周期为应用程序内活动的状态管理提供了一个强大的框架,但理解和实现它可能很棘手。 本章介绍了活动在其生存期中可能经历的不同状态,以及与这些状态关联的生命周期方法。 接下来,提供了关于在每种方法中应执行哪种逻辑的指导。