任务和返回堆栈
任务是用户在执行某项工作时与之互动的一系列 Activity 的集合。这些 Activity 按照每个 Activity 打开的顺序排列在一个返回堆栈中。
如果用户按返回按钮,这个新的 Activity 即会完成并从堆栈中退出。
Android 7.0(API 级别 24)及更高版本支持多窗口环境(即分屏操作),当应用在这种环境中同时运行时,系统会单独管理每个窗口的任务;而每个窗口可能包含多项任务。
如果打开的某个任务是之前没有存在过的则,创建新的任务并将该应用的主Activity作为堆栈的根Activity打开。
返回堆栈的Activity顺序
在当前 Activity 启动另一个 Activity 时,新的 Activity 将被推送到堆栈顶部并获得焦点。上一个 Activity 仍保留在堆栈中,但会停止。当 Activity 停止时,系统会保留其界面的当前状态。当用户按返回按钮时,当前 Activity 会从堆栈顶部退出(该 Activity 销毁),上一个 Activity 会恢复(界面会恢复到上一个状态)。**堆栈中的 Activity 永远不会重新排列,只会被送入和退出,在当前 Activity 启动时被送入堆栈,在用户使用返回按钮离开时从堆栈中退出。**因此,返回堆栈按照“后进先出”的对象结构运作。
示意图:
如果用户继续按返回,则堆栈中的 Activity 会逐个退出,以显示前一个 Activity,直到用户返回到主屏幕(或任务开始时运行的 Activity)。移除堆栈中的所有 Activity 后,该任务将不复存在。
任务的切换
任务是一个整体单元,当用户开始一个新任务或通过主屏幕按钮进入主屏幕时,任务可移至“后台”。**在后台时,任务中的所有 Activity 都会停止,但任务的返回堆栈会保持不变,当其他任务启动时,当前任务只是失去了焦点。**这样一来,任务就可以返回到“前台”,以便用户可以从他们离开的地方继续操作。举例来说,假设当前任务(任务 A)的堆栈中有 3 个 Activity,当前 Activity 下有 2 个 Activity。用户按主屏幕按钮,然后从应用启动器中启动新应用。主屏幕出现后,任务 A 转到后台。当新应用启动时,系统会启动该应用的任务(任务 B),该任务具有自己的 Activity 堆栈。与该应用互动后,用户再次返回到主屏幕并选择最初启动任务 A 的应用。现在,任务 A 进入前台,其堆栈中的所有三个 Activity 都完好如初,堆栈顶部的 Activity 恢复运行。此时,用户仍可通过以下方式切换到任务 B:转到主屏幕并选择启动该任务的应用图标(或者从最近使用的应用屏幕中选择该应用的任务)
值得注意的是,多个任务可以同时在后台进行。但是,如果用户同时运行很多后台任务,系统可能会为了恢复内存而开始销毁后台 Activity,导致 Activity 状态丢失。
某个Activity被多次实例化
**由于返回堆栈中的 Activity 不会被重新排列,如果您的应用允许用户从多个 Activity 启动特定的 Activity,系统便会创建该 Activity 的新实例并将其推送到堆栈中(而不是将该 Activity 的某个先前的实例移至堆栈顶部)。**这样一来,应用中的一个 Activity 就可能被多次实例化(甚至是从其他任务对其进行实例化)。因此,如果用户使用返回按钮向后导航,Activity 的每个实例将按照它们被打开的顺序显示出来(每个实例都有自己的界面状态)。不过,如果您不希望某个 Activity 被实例化多次,可以修改此行为。
Activity 和任务的默认行为总结如下:
- 当 Activity A 启动 Activity B 时,Activity A 会停止,但系统会保留其状态(例如滚动位置和输入到表单中的文本)。如果用户在 Activity B 中按返回按钮,系统会恢复 Activity A 及其状态。
- 当用户通过按主屏幕按钮离开任务时,当前 Activity 会停止,其任务会转到后台。系统会保留任务中每个 Activity 的状态。如果用户稍后通过点按该任务的启动器图标来恢复该任务,该任务会进入前台并恢复堆栈顶部的 Activity。
- 如果用户按返回按钮,当前 Activity 将从堆栈中退出并销毁。堆栈中的上一个 Activity 将恢复。Activity 被销毁后,系统不会保留该 Activity 的状态。
- Activity 可以多次实例化,甚至是从其他任务对其进行实例化。
任务管理
如果希望应用中的某个 Activity 在启动时开启一个新的任务(而不是被放入当前的任务中),或者当启动某个 Activity 时,希望调用它的一个现有实例(而不是在返回堆栈顶部创建一个新实例),或者希望在用户离开任务时清除返回堆栈中除根 Activity 以外的所有 Activity。
可以借助清单元素中的属性以及您传递给 startActivity()
的 intent 中的标记来实现上述目的。
在这方面,您可以使用的主要 属性包括:
taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch
您可以使用的主要 intent 标记包括:
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_SINGLE_TOP
自定义启动模式
可以通过启动模式定义 Activity 的新实例如何与当前任务关联。您可以通过两种方式定义不同的启动模式:
-
使用清单文件
当您在清单文件中声明 Activity 时,您可以指定该 Activity 在启动时如何与任务关联。
-
使用 Intent 标记
当您调用
startActivity()
时,可以在Intent
中添加一个标记,用于声明新 Activity 如何(或是否)与当前任务相关联。
因此,如果 Activity A 启动 Activity B,Activity B 可在其清单中定义如何与当前任务相关联(如果关联的话),Activity A 也可以请求 Activity B 应该如何与当前任务关联。如果两个 Activity 都定义了 Activity B 应如何与任务关联,将优先遵循 Activity A 的请求(在 intent 中定义),而不是 Activity B 的请求(在清单中定义)。
值得注意的是,有些启动模式仅可以通过intent标记定义,有些仅可以使用清单定义。
使用清单文件自定义启动模式
在清单文件中声明 Activity 时,可以使用元素的 launchMode
属性指定 Activity 应该如何与任务关联。
示例代码:
<activity android:name=".MainActivity"
android:launchMode="modeName">
....
</activity>
launchMode
属性说明了 Activity 应如何启动到任务中:
-
“standard”(默认模式)
系统在启动该 Activity 的任务中创建 Activity 的新实例,并将 intent 传送给该实例。Activity 可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例。
-
“singleTop”
如果当前任务的顶部已存在 Activity 的实例,则系统会通过调用其
onNewIntent()
方法来将 intent 转送给该实例,而不是创建 Activity 的新实例。Activity 可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例(但前提是返回堆栈顶部的 Activity 不是该 Activity 的现有实例)。 -
“singleTask”
系统会创建新任务,并实例化新任务的根 Activity。但是,如果另外的任务中已存在该 Activity 的实例,则系统会通过调用其
onNewIntent()
方法将 intent 转送到该现有实例,而不是创建新实例。Activity 一次只能有一个实例存在。值的注意的是,虽然新的Activity在新任务中启动,但是用户按返回按钮仍会返回到上一个Activity,即切换了前台Task。
-
“singleInstance”
与
"singleTask"
相似,唯一不同的是系统不会将任何其他 Activity 启动到包含该实例的任务中。该 Activity 始终是其任务唯一的成员;由该 Activity 启动的任何 Activity 都会在其他的任务中打开。
无论 Activity 是在新任务中启动的,还是在和启动它的 Activity 相同的任务中启动,用户按返回按钮都会回到上一个 Activity。但是,如果启动了指定 singleTask
启动模式的 Activity,而后台任务中已存在该 Activity 的实例,则系统会将该后台任务整个转到前台运行。此时,返回堆栈包含了转到前台的任务中的所有 Activity,这些 Activity 都位于堆栈的顶部。
即在声明了"singleTask"的Activity在从后台转往前台时,其所在的任务全部返回到任栈顶部。
示意图:
模式单词的含义都是以Task做为主语的。
使用Intent标记自定义启动模式
启动 Activity 时,可以在传送给 startActivity()
的 intent 中添加相应的标记来修改 Activity 与其任务的默认关联。
示例代码:
@Override
public void startActivity(Intent intent) {
super.startActivity(intent);
intent.setFlags(Intent.FLAG_ACTIVITY_XXXX);
}
可以使用如下标记来修改默认行为:
-
“FLAG_ACTIVITY_NEW_TASK”
在新任务中启动 Activity。如果您现在启动的 Activity 已经有任务在运行,则系统会将该任务转到前台并恢复其最后的状态,而 Activity 将在
onNewIntent()
中收到新的 intent。与在清单文件中launchMode取值为“singleTask”时行为相同。
-
“FLAG_ACTIVITY_SINGLE_TOP”
如果要启动的 Activity 是当前 Activity(即位于返回堆栈顶部的 Activity),则现有实例会收到对
onNewIntent()
的调用,而不会创建 Activity 的新实例。与在清单文件中launchMode取值为“singleTop”时行为相同。
-
“FLAG_ACTIVITY_CLEAR_TOP”
如果要启动的 Activity 已经在当前任务中运行,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过
onNewIntent()
将此 intent 传送给它的已恢复实例(现在位于堆栈顶部)。
处理亲和性
“亲和性”表示 Activity 倾向于属于哪个任务。默认情况下,同一应用中的所有 Activity 彼此具有亲和性。因此,在默认情况下,同一应用中的所有 Activity 都倾向于位于同一任务。
在不同应用中定义的 Activity 可以具有相同的亲和性,或者在同一应用中定义的 Activity 也可以被指定不同的任务亲和性。
可以使用元素的 taskAffinity
属性修改任何给定 Activity 的亲和性。
亲和性可以在以下两种情况下发挥作用:
-
当启动Activity的intent包含FLAG_ACTIVITY_NEW_TASK标记时。
默认情况下,新 Activity 会启动到调用
startActivity()
的 Activity 的任务中。它会被推送到调用方 Activity 所在的返回堆栈中。但是,如果传递给startActivity()
的 intent 包含FLAG_ACTIVITY_NEW_TASK
标记,则系统会寻找其他任务来容纳新 Activity。通常会是一个新任务,但也可能不是。如果已存在与新 Activity 具有相同亲和性的现有任务,则会将 Activity 启动到该任务中。如果不存在,则会启动一个新任务。 -
当Activity的allowTaskReparenting属性取值为"true"时。
在这种情况下,一旦和 Activity 有亲和性的任务进入前台运行,Activity 就可从其启动的任务转移到该任务。
举例来说,假设一款旅行应用中定义了一个报告特定城市天气状况的 Activity。该 Activity 与同一应用中的其他 Activity 具有相同的亲和性(默认应用亲和性),并通过此属性支持重新归属。当您的某个 Activity 启动该天气预报 Activity 时,该天气预报 Activity 最初会和您的 Activity 同属于一个任务。不过,当旅行应用的任务进入前台运行时,该天气预报 Activity 就会被重新分配给该任务并显示在其中。
清除返回栈
如果用户离开任务较长时间,系统会清除任务中除根 Activity 以外的所有 Activity。当用户再次返回到该任务时,只有根 Activity 会恢复。系统之所以采取这种行为方式是因为,经过一段时间后,用户可能已经放弃了之前执行的操作,现在返回任务是为了开始某项新的操作。
可以使用一些 Activity 属性来修改此行为:
-
alwaysRetainTaskState
如果在任务的根 Activity 中将该属性设为
"true"
,则不会发生上述默认行为。即使经过很长一段时间后,任务仍会在其堆栈中保留所有 Activity。 -
clearTaskOnLaunch
如果在任务的根 Activity 中将该属性设为
"true"
,那么只要用户离开任务再返回,堆栈就会被清除到只剩根 Activity。也就是说,它与alwaysRetainTaskState
正好相反。用户始终会返回到任务的初始状态,即便只是短暂离开任务也是如此。 -
finishOnTaskLaunch
该属性与
clearTaskOnLaunch
类似,但它只会作用于单个 Activity 而非整个任务。它还可导致任何 Activity 消失,包括根 Activity。如果将该属性设为"true"
,则 Activity 仅在当前会话中归属于任务。如果用户离开任务再返回,则该任务将不再存在。
启动任务
您可以设置一个 Activity 作为任务的入口点,方法是为该 Activity 提供一个 intent 过滤器,并将 "android.intent.action.MAIN"
作为指定操作,将 "android.intent.category.LAUNCHER"
作为指定类别。
示例代码:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
第二个作用非常重要:用户必须能够离开任务,之后再使用此 Activity 启动器返回到该任务。因此,只有当 Activity 具有 ACTION_MAIN
和 CATEGORY_LAUNCHER
过滤器时,才应使用 "singleTask"
和 "singleInstance"
这两种启动模式,它们会将 Activity 标记为始终启动任务。比如,可以想象一下,如果缺少该过滤器会发生什么情况:intent 会启动 "singleTask"
Activity,随之启动新任务,用户花了一些时间在该任务上。然后,用户按主屏幕按钮。此时,该任务会转到后台,不再可见。现在,用户无法返回到该任务,因为它未显示在应用启动器中。