活动生命周期

活动是 Android 应用程序的基本构建基块,它们可存在于多种不同的状态中。 活动生命周期以实例化开始,以销毁结尾,包括介于两者之间的许多状态。 当活动更改状态时,将调用适当的生命周期事件方法,通知活动即将发生的状态更改,并允许它执行代码以适应该更改。 本文探讨了活动生命周期,并解释了活动在每个状态改变期间的责任和义务,以确保应用程序行为良好且可靠。

活动生命周期概述

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

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

这点非常重要,应用开发人员需要分析每个活动的需求,从而确定需要实现的活动生命周期中的方法。 无法执行此作可能会导致应用程序不稳定、崩溃、资源膨胀,甚至可能导致基础 OS 不稳定。

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

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

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

活动生命周期

Android 活动生命周期包括活动类中公开的方法集合,这些方法为开发人员提供资源管理框架。 此框架允许开发人员满足应用程序中每个活动的唯一状态管理要求,并正确处理资源管理。

活动状态

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

活动状态图

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

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

  2. 已暂停 – 当设备进入睡眠状态或该活动仍可见,但部分被新的透明或非全尺寸活动遮挡时,该活动被视为已暂停。 暂停的任务仍然正常工作,这就是说,它们维持所有状态和成员信息,并与窗口管理器保持连接。 这被视为 Android 中的第二个最高优先级活动,因此,如果终止此活动将满足使活动/运行活动保持稳定且响应性所需的资源要求,OS 才会终止此活动。

  3. 已停止/后台 – 被其他活动完全遮挡的活动被视为已停止或处于后台。 停止的活动仍会尽量保留其状态和成员信息,但停止的活动被视为三种状态的最低优先级,因此,OS 将首先终止处于此状态的活动,以满足较高优先级活动的资源要求。

  4. 已重启 – 在生命周期中,从暂停到停止的任何位置的活动都可以由 Android 从内存中删除。 如果用户导航回活动,则必须重新启动它,还原到其以前保存的状态,然后显示给用户。

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

让问题变得更加复杂的是,Android 又增加了一个麻烦,称为配置更改。 配置更改是快速的活动销毁/重建周期,当活动的配置发生变化时,例如,当设备旋转(活动需要在横向或纵向模式下重新构建),当键盘显示时(活动有机会调整其大小),或者设备被放置在底座中等情况时,会发生此类周期。

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

活动生命周期方法

Android SDK 和 Xamarin.Android 框架扩展提供了一个强大的模型,用于管理应用程序中的活动状态。 当活动状态发生更改时,操作系统会通知该活动,并调用特定的方法。 下图说明了与活动生命周期相关的这些方法:

活动生命周期流程图

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

让我们检查以下每个生命周期方法及其用法:

OnCreate

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

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

OnCreate 采用 Bundle 参数,该参数是用于在活动之间存储和传递状态信息和对象(如果捆绑包不为 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

系统始终在OnCreate完成后调用OnStart。 如果活动需要在某活动可见之前执行任何特定的任务,例如刷新活动中视图的当前值,则活动可能会替代此方法。 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。 如果活动需要,则应替代此方法:

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

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

  • 降低帧速率和暂停动画

  • 取消注册外部事件处理程序或通知处理程序(即与服务绑定的事件处理程序)。 必须这样做以防止 Activity 内存泄漏。

  • 同样,如果活动已显示任何对话框或警报,则必须使用 .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 不调用。 大多数活动不会实现此方法,因为大多数清理和关闭工作已在OnPause方法和OnStop方法中完成。 通常会替代 OnDestroy 方法来清理可能泄漏资源的长期运行的任务。 其中一个示例可能是OnCreate中启动的后台线程。

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

OnRestart

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

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

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

“后退”与Home

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

返回和主页按钮

这两个按钮之间存在细微的区别,尽管它们似乎具有相同的效果,即在后台放置应用程序。 当用户单击“后退”按钮时,他们告诉 Android 他们已完成该活动。 Android 将销毁活动。 相比之下,当用户单击“开始”按钮时,活动只是放置在后台 - Android 不会终止活动。

管理整个生命周期的状态

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

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

  2. 创建将保存复杂值(如位图)的自定义类。 Android 将使用此自定义类保存状态。

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

本指南介绍前两个选项。

捆绑包状态

保存实例状态的主要选项是使用称为 Bundle 的键/值字典对象。 请记住,当活动被创建时,OnCreate方法接收到一个捆绑包作为参数,该捆绑包可用于恢复实例状态。 不建议将捆绑包用于更复杂的数据,这些数据不会快速或轻松地序列化为键/值对(如位图):应将其用于字符串等简单值。

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

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

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

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

捆绑状态流程图

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的值,因为bundlenull,如下图所示:

显示器不显示先前数据

若要保留此示例中 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,当用户输入某些数据并旋转设备时,数据仍然会显示,如下所示:

数据在横向模式下保留

OnRestoreInstanceState

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

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

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

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

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

捆绑限制

尽管 OnSaveInstanceState 可以轻松保存暂时性数据,但它有一些限制:

  • 并非所有情况下都调用它。 例如,按 HomeBack 退出活动不会导致 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);
  }
}

此代码从格式化为 JSON 的 Web 检索结果,对其进行分析,然后在列表中显示结果,如以下屏幕截图所示:

屏幕上显示的结果

当发生配置更改(例如,当设备轮换时)时,代码将重复该过程。 若要重复使用最初检索的结果,而不会导致不必要的冗余网络调用,我们可以使用 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 返回的对象将导致 Activity 泄漏,如以下代码所示:

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