任务和后退堆栈

任务 是用户在您的应用中尝试执行某项操作时会与之交互的一系列活动。这些活动按每个活动打开的顺序排列在一个堆栈中,该堆栈称为回退堆栈

例如,电子邮件应用可能有一个活动用于显示新消息列表。当用户选择一条消息时,会打开一个新活动以查看该消息。此新活动将添加到回退堆栈中。然后,当用户点击或手势返回时,该新活动将完成并从堆栈中弹出。

任务及其回退堆栈的生命周期

设备主屏幕是大多数任务的起点。当用户点击应用启动器或主屏幕上的应用图标或快捷方式时,该应用的任务将进入前台。如果该应用不存在任何任务,则会创建一个新任务,并且该应用的 主活动 将作为堆栈中的根活动打开。

当当前活动启动另一个活动时,新活动将被推送到堆栈顶部并获得焦点。前一个活动将保留在堆栈中,但会被停止。当活动停止时,系统会保留其用户界面的当前状态。当用户执行返回操作时,当前活动将从堆栈顶部弹出并销毁。前一个活动将恢复,并且其 UI 的先前状态将被恢复。

堆栈中的活动永远不会重新排列,它们只会在当前活动启动它们时被推入堆栈,并在用户通过“返回”按钮或手势将其关闭时被弹出。因此,返回堆栈的操作方式类似于**后进先出**的 对象结构。图 1 展示了活动被推入和弹出返回堆栈的时间线。

图 1. 每个新活动在任务中添加一个条目到返回堆栈的表示。当用户点击或使用“返回”手势时,当前活动会被销毁,前一个活动将会恢复。

当用户继续点击或使用“返回”手势时,堆栈中的每个活动都会被弹出,以显示之前的活动,直到用户返回主屏幕或返回任务开始时正在运行的活动。当所有活动都从堆栈中移除时,任务就不再存在。

根启动器活动的返回点击行为

根启动器活动是指那些在 意图过滤器 中同时声明了 ACTION_MAINCATEGORY_LAUNCHER 的活动。这些活动是独一无二的,因为它们充当用户从应用程序启动器进入应用程序的入口点,并用于启动任务

当用户从根启动器活动点击或使用“返回”手势时,系统会根据设备运行的 Android 版本来处理该事件。

Android 11 及以下版本上的系统行为
系统会结束该活动。
Android 12 及以上版本上的系统行为

系统会将活动及其任务移到后台,而不是结束该活动。此行为与用户使用“主页”按钮或手势从应用程序导航时默认的系统行为一致。

在大多数情况下,这种行为意味着用户可以更快地从 暖启动状态 恢复您的应用程序,而不是必须从 冷启动状态 完全重新启动应用程序。

如果您需要 提供自定义返回导航,建议使用 AndroidX Activity API,而不是覆盖 onBackPressed()。如果没有任何组件拦截系统返回点击,AndroidX Activity API 会自动转而使用相应的系统行为。

但是,如果您的应用程序覆盖了 onBackPressed() 来处理返回导航并结束活动,请更新您的实现以调用 super.onBackPressed(),而不是结束活动。调用 super.onBackPressed() 会在适当的时候将活动及其任务移到后台,并为跨应用程序的用户提供更一致的导航体验。

后台和前台任务

图 2. 两个任务:任务 B 在前台接收用户交互,而任务 A 在后台等待恢复。

任务是一个具有凝聚力的单元,当用户开始一个新的任务或进入主屏幕时,它可以移动到**后台**。在后台时,任务中的所有活动都被停止,但任务的返回堆栈保持完整——任务会失去焦点,而另一个任务会执行,如图 2 所示。然后任务可以返回**前台**,以便用户从他们离开的地方继续操作。

考虑以下针对当前任务 A 的任务流程,该任务在其堆栈中有三个活动,包括当前活动下的两个活动

  1. 用户使用“主页”按钮或手势,然后从应用程序启动器启动一个新的应用程序。

    当主屏幕出现时,任务 A 会进入后台。当新应用程序启动时,系统会为该应用程序启动一个具有自己的活动堆栈的任务(任务 B)。

  2. 在与该应用程序交互之后,用户再次返回主屏幕,然后选择最初启动任务 A 的应用程序。

    现在,任务 A 会来到前台——其堆栈中的所有三个活动都保持完整,并且堆栈顶部的活动会恢复。此时,用户也可以通过返回主屏幕并选择启动该任务的应用程序图标,或从最近使用的应用程序屏幕选择应用程序的任务,来切换回任务 B。

多个活动实例

图 3. 单个活动可以多次实例化。

由于返回堆栈中的活动永远不会重新排列,如果您的应用程序允许用户从多个活动启动特定活动,系统会创建该活动的新的实例并将其推入堆栈,而不是将任何先前的活动实例带到顶部。因此,您的应用程序中的一个活动可能被多次实例化,即使是从不同的任务,如图 3 所示。

如果用户使用“返回”按钮或手势向后导航,活动的实例将按照它们打开的顺序显示,每个实例都有自己的 UI 状态。但是,如果您不希望活动被实例化多次,可以修改此行为。有关此方面的更多信息,请参阅有关管理任务的部分。

多窗口环境

当应用程序在 多窗口环境 中同时运行时,该环境在 Android 7.0(API 级别 24)及更高版本中受支持,系统会为每个窗口分别管理任务。每个窗口可以有多个任务。这对 在 Chromebook 上运行的 Android 应用程序 也是如此:系统会按窗口为基础管理任务或任务组。

生命周期回顾

总结活动和任务的默认行为

  • 当活动 A 启动活动 B 时,活动 A 会被停止,但系统会保留其状态,例如其滚动位置和在表单中输入的任何文本。如果用户在活动 B 中点击或使用“返回”手势,活动 A 会恢复,其状态也会恢复。

  • 当用户使用“主页”按钮或手势离开任务时,当前活动会被停止,其任务会进入后台。系统会保留任务中每个活动的状态。如果用户稍后通过选择启动任务的启动器图标来恢复任务,则该任务会来到前台,并恢复堆栈顶部的活动。

  • 如果用户点击或使用“返回”手势,当前活动将从堆栈中弹出并销毁。堆栈中的前一个活动将会恢复。当活动被销毁时,系统**不会**保留活动的狀態。

    此行为对于根启动器活动 来说有所不同,前提是您的应用程序在运行 Android 12 或更高版本的设备上运行。

  • 活动可以多次实例化,即使是从其他任务。

管理任务

Android 通过将所有依次启动的活动放在同一个任务中,并按照后进先出的方式排列,来管理任务和返回堆栈。这对于大多数应用程序来说非常有效,您通常不必担心活动如何与任务关联或它们如何在返回堆栈中存在。

但是,您可能决定要中断正常的行为。例如,您可能希望应用程序中的一个活动在启动时开始一个新的任务,而不是放在当前任务中。或者,当您启动一个活动时,您可能希望将现有实例带到前台,而不是在返回堆栈顶部创建一个新的实例。或者您可能希望在用户离开任务时,清除返回堆栈中除根活动之外的所有活动。

您可以使用<activity> 清单元素中的属性和传递给 startActivity() 的意图中的标志来完成这些操作以及更多操作。

以下是可以用来管理任务的主要<activity> 属性

以下是可以使用的主要意图标志

以下部分将讨论如何使用这些清单属性和意图标志来定义活动如何与任务关联以及它们如何在返回堆栈中运行。

还将讨论任务和活动如何在最近使用的应用程序屏幕中表示和管理的注意事项。通常,您让系统定义任务和活动如何在最近使用的应用程序屏幕中表示,您不需要修改此行为。有关更多信息,请参阅最近使用的应用程序屏幕

定义启动模式

启动模式允许您定义如何将活动的新的实例与当前任务关联。您可以通过两种方式定义启动模式,如下面的部分所述

  • 使用清单文件

    当您在清单文件中声明一个活动时,您可以指定活动在启动时如何与任务关联。

  • 使用意图标志

    当您调用 startActivity() 时,您可以在 Intent 中包含一个标志,该标志声明新活动如何(或是否)与当前任务关联。

因此,如果活动 A 启动活动 B,则活动 B 可以在其清单中定义它如何与当前任务关联,而活动 A 可以使用一个意图标志来请求活动 B 如何与当前任务关联。

如果这两个活动都定义了活动 B 如何与任务关联,则意图中定义的活动 A 的请求将优先于其清单中定义的活动 B 的请求。

使用清单文件定义启动模式

在清单文件中声明一个活动时,可以使用 <activity> 元素的 launchMode 属性来指定活动如何与任务关联。

您可以将五个启动模式分配给 launchMode 属性

  1. "standard"
    默认模式。系统会在启动该活动的任务中创建活动的新的实例,并将意图路由到该实例。活动可以多次实例化,每个实例可以属于不同的任务,一个任务可以有多个实例。
  2. "singleTop"
    如果活动实例已存在于当前任务的顶部,系统会通过调用其 onNewIntent() 方法将意图路由到该实例,而不是创建活动的新实例。活动被多次实例化,每个实例可以属于不同的任务,一个任务可以有多个实例(但前提是回退栈顶部的活动不是活动 的现有实例)。

    例如,假设一个任务的回退栈包含根活动 A,其上面有活动 B、C 和 D(因此栈为 A-B-C-D,D 位于顶部)。一个意图到达一个类型为 D 的活动。如果 D 具有默认的 "standard" 启动模式,则会启动该类的新的实例,并且栈变为 A-B-C-D-D。但是,如果 D 的启动模式为 "singleTop",则现有 D 实例会通过 onNewIntent() 接收意图,因为它位于栈顶,并且栈保持 A-B-C-D。另一方面,如果一个意图到达一个类型为 B 的活动,则即使其启动模式为 "singleTop",也会将 B 的新实例添加到栈中。

  3. "singleTask"
    系统会在新任务的根目录处创建活动,或在具有相同关联性的现有任务上定位活动。如果活动 的实例已存在,系统会通过调用其 onNewIntent() 方法将意图路由到现有实例,而不是创建新的实例。同时,位于其上面的所有其他活动都会被销毁。
  4. "singleInstance".
    行为与 "singleTask" 相同,只是系统不会将任何其他活动启动到包含该实例的任务中。活动 始终是其任务的唯一成员。此活动启动的任何活动都会在单独的任务中打开。
  5. "singleInstancePerTask".
    该活动只能作为任务的根活动运行,即创建该任务的第一个活动,因此一个任务中只能有一个该活动的实例。与 singleTask 启动模式相比,如果设置了 FLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_NEW_DOCUMENT 标志,则此活动可以在不同任务中以多个实例启动。

作为另一个示例,Android 浏览器应用通过在 <activity> 元素中指定 singleTask 启动模式来声明网络浏览器活动始终在其自己的任务中打开。这意味着,如果您的应用发出一个打开 Android 浏览器的意图,其活动不会被放置到与您的应用相同的任务中。相反,浏览器会启动一个新的任务,或者如果浏览器已经有一个任务在后台运行,则该任务会被带到前台以处理新的意图。

无论活动是在新任务中启动还是在与启动它的活动相同的任务中启动,返回按钮和手势始终会将用户带回到先前的活动。但是,如果您启动一个指定了 singleTask 启动模式的活动,并且该活动的实例存在于后台任务中,则整个任务会被带到前台。此时,回退栈中包含从任务中带到前台的所有活动,它们位于栈顶。图 4 显示了这种类型的场景。

图 4. 表示具有 "singleTask" 启动模式的活动如何添加到回退栈中。如果该活动已经是后台任务的一部分,并且该任务有自己的回退栈,则整个回退栈也会向前移动,位于当前任务的顶部。

有关在清单文件中使用启动模式的更多信息,请参阅 <activity> 元素文档。

使用意图标志定义启动模式

在启动活动时,您可以通过在传递给 startActivity() 的意图中包含标志来修改活动与其任务的默认关联性。您可以用来修改默认行为的标志如下:

FLAG_ACTIVITY_NEW_TASK

系统会在新任务中启动活动。如果活动正在启动的任务已经在运行,则该任务会恢复其最后状态,并带到前台,并且活动会在 onNewIntent() 中接收到新的意图。

这会产生与上一部分讨论的 "singleTask" launchMode 值相同的行为。

FLAG_ACTIVITY_SINGLE_TOP

如果活动正在启动并且是当前活动,位于回退栈的顶部,则现有实例会接收到对 onNewIntent() 的调用,而不是创建 活动 的新实例。

这会产生与上一部分讨论的 "singleTop" launchMode 值相同的行为。

FLAG_ACTIVITY_CLEAR_TOP

如果活动正在启动并且已经在当前任务中运行,则系统会销毁位于其上面的所有其他活动,而不是启动该活动的 新实例。意图会通过 onNewIntent() 传递到已恢复的活动实例,该实例现在位于顶部。

没有 launchMode 属性的值可以产生这种行为。

FLAG_ACTIVITY_CLEAR_TOP 通常与 FLAG_ACTIVITY_NEW_TASK 一起使用。当一起使用时,这些标志会定位其他任务中 的现有活动,并将其放置在一个可以响应意图的位置。

处理关联性

关联性指示活动 "首选" 属于哪个任务。默认情况下,来自同一应用的所有活动都具有相互关联性:它们 "首选" 位于同一个任务中。

但是,您可以修改活动的默认关联性。不同应用中定义的活动可以共享关联性,同一个应用中定义的活动可以被分配不同的任务关联性。

您可以使用 taskAffinity 属性(<activity> 元素的属性)来修改活动的关联性。

taskAffinity 属性需要一个字符串值,该值必须不同于 <manifest> 元素中声明的默认包名称,因为系统使用该名称来标识应用的 默认任务关联性。

关联性在两种情况下会发挥作用:

  1. 当启动活动的 intent 包含 FLAG_ACTIVITY_NEW_TASK 标志时。

    默认情况下,新的活动会被启动到调用 startActivity() 的活动的 任务中。它会被推送到与调用者相同的回退栈中。

    但是,如果传递给 startActivity() 的 intent 包含 FLAG_ACTIVITY_NEW_TASK 标志,则系统会寻找一个不同的任务来容纳新的活动。通常,这是一个新的任务。但是,它并不一定需要是新的任务。如果有一个现有任务与新的活动的关联性相同,则该活动会被启动到该任务中。如果没有,则会开始一个新的任务。

    如果此标志导致活动开始一个新的任务,并且用户使用“主页”按钮或手势离开它,则用户必须有一种方法可以返回到该任务。某些实体(例如通知管理器)始终在外部任务中启动活动,而不是作为其自身的一部分启动活动,因此它们始终在传递给 startActivity() 的意图中放置 FLAG_ACTIVITY_NEW_TASK

    如果可能会使用此标志的外部实体可以调用您的活动,请确保用户有独立的方法可以返回到已启动的任务,例如使用启动器图标,其中该任务的根活动具有 CATEGORY_LAUNCHER 意图过滤器。有关更多信息,请参阅有关 启动任务 的部分。

  2. 当活动的 allowTaskReparenting 属性设置为 "true" 时。

    在这种情况下,活动可以从它启动的任务移动到它具有关联性的任务,当该任务转到前台时。

    例如,假设一个活动报告所选城市的天气状况,该活动被定义为旅行应用的一部分。它与同一应用中的其他活动具有相同的关联性,即默认的应用关联性,并且可以使用此属性重新指定其父级。

    当您的活动之一启动天气报告活动时,它最初属于与您的活动相同的任务。但是,当旅行应用的任务转到前台时,天气报告活动会被重新分配到该任务并显示在该任务中。

清除回退栈

如果用户长时间离开任务,系统会清除该任务中的所有活动,除了根活动。当用户返回该任务时,只有根活动会被恢复。系统之所以这样运作,是基于一个假设:经过一段较长的时间后,用户已经放弃了之前正在做的事情,并重新返回到该任务以开始新的工作。

有一些活动属性可以用来修改这种行为。

alwaysRetainTaskState
当此属性在任务的根活动中设置为 "true" 时,上面描述的默认行为不会发生。即使经过长时间,任务也会保留其堆栈中的所有活动。
clearTaskOnLaunch

当此属性在任务的根活动中设置为 "true" 时,无论何时用户离开任务并返回,任务都会被清除到根活动。换句话说,它与 alwaysRetainTaskState 相反。用户总是返回到任务的初始状态,即使只离开了任务片刻。

finishOnTaskLaunch

此属性类似于 clearTaskOnLaunch,但它只针对单个活动,而不是整个任务。它还可以导致除根活动之外的所有活动都完成。当它被设置为 "true" 时,活动只在当前会话中保留在任务中。如果用户离开然后返回到任务,它将不再存在。

启动一个任务

可以通过给一个活动设置一个意图过滤器来将其设置为任务的入口点,该意图过滤器指定 "android.intent.action.MAIN" 作为操作,并指定 "android.intent.category.LAUNCHER" 作为类别。

<activity ... >
    <intent-filter ... >
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    ...
</activity>

这种意图过滤器会导致活动在应用启动器中显示一个图标和标签,为用户提供了一种方法来启动活动,并在活动启动后随时返回到它创建的任务。

第二个能力很重要。用户必须能够离开一个任务,然后使用这个活动启动器返回到该任务。因此,只有当活动具有 ACTION_MAINCATEGORY_LAUNCHER 过滤器时,才使用将活动标记为始终启动一个任务的两种启动模式,"singleTask""singleInstance"

例如,想象一下如果缺少过滤器会发生什么:一个意图启动一个 "singleTask" 活动,启动一个新任务,用户在该任务中工作一段时间。然后用户使用 Home 按钮或手势。现在,该任务被发送到后台,不可见。现在用户无法返回到该任务,因为它不在应用启动器中表示。

对于不希望用户能够返回到活动的那些情况,请将 <activity> 元素的 finishOnTaskLaunch 设置为 "true"。有关更多信息,请参见有关 清除后退堆栈 的部分。

有关如何在最近使用应用屏幕中表示和管理任务和活动的信息,请参见 最近使用应用屏幕

更多资源