任务和返回堆栈

任务是用户在尝试执行应用中的操作时交互的一系列活动。这些活动按每个活动打开的顺序排列在一个称为返回堆栈的堆栈中。

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

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

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

当当前活动启动另一个活动时,新活动将推送到堆栈顶部并获得焦点。之前的活动保留在堆栈中,但已停止。当活动停止时,系统会保留其用户界面的当前状态。当用户执行返回操作时,当前活动将从堆栈顶部弹出并销毁。之前的活动恢复,并恢复其 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>属性

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

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

还讨论了如何在最近使用的屏幕中表示和管理任务和活动。通常,您允许系统定义任务和活动如何在最近使用的屏幕中表示,并且您不需要修改此行为。有关更多信息,请参阅最近使用的屏幕

定义启动模式

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

因此,如果活动 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启动模式来声明 Web 浏览器活动始终在其自身的任务中打开。这意味着,如果您的应用发出打开 Android 浏览器的意图,则其活动不会与您的应用位于同一任务中。相反,浏览器会启动一个新任务,或者如果浏览器已经在后台运行了一个任务,则该任务会被调到前台来处理新的意图。

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

图 4. 带有启动模式"singleTask"的活动添加到返回堆栈的方式。如果活动已经是具有自身返回堆栈的后台任务的一部分,则整个返回堆栈也会向前移动,位于当前任务的顶部。

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

使用 Intent 标志定义启动模式

启动活动时,可以通过将标志包含在传递给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. 当启动活动的意图包含FLAG_ACTIVITY_NEW_TASK标志时。

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

    但是,如果传递给startActivity()的意图包含FLAG_ACTIVITY_NEW_TASK标志,系统会查找不同的任务来容纳新活动。通常,这是一个新任务。但是,它不必是新任务。如果存在与新活动具有相同关联性的现有任务,则活动将启动到该任务中。如果没有,它将开始一个新任务。

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

    如果可能使用此标志的外部实体可以调用您的活动,请注意用户是否有独立的方法返回到已启动的任务,例如使用启动器图标,其中任务的根活动具有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" 活动,启动一个新任务,用户在该任务中工作一段时间。然后用户使用主页按钮或手势。该任务现在被发送到后台并且不可见。现在用户无法返回到该任务,因为它没有在应用程序启动器中表示。

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

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

更多资源