任务和返回栈
https://developer.android.google.cn/guide/components/activities/tasks-and-back-stack.html --翻译水平有限,欢迎指正
任务是用户在执行某项工作时与之交互的一系列activity的集合。activity按照打开的顺序被安排在返回栈( back stack)中。例如,一个电子邮件应用程序可能有一个activity来显示新消息的列表。当用户选择一条消息时,将打开一个新的activity来查看该消息。这个新activity被添加到返回栈中。如果用户按下 Back 按钮,新activity就结束了,并从堆栈中弹出。
当多个应用程序在一个多窗口环境(Android 7.0 (API级别24)和更高版本支持)中同时运行时,系统为每个窗口单独地管理任务;每个窗口可能有多个任务。在chromebook上运行的Android应用程序也是如此:系统在每个窗口的基础上管理任务或任务组。
设备主屏幕是大多数任务的起点。当用户在应用程序启动器(或主屏幕上的快捷方式)中触摸一个图标时,该应用程序的任务就会出现在前台。如果应用程序没有任务(最近没有使用该应用程序),那么就会创建一个新的任务,该应用程序的“main” activity将作为堆栈中的根activity打开。
当当前的activity启动另一个activity时,新的activity将被推到堆栈的顶部并获得焦点。之前的activity仍然在堆栈中,但已经停止。当activity停止时,系统保留其用户界面的当前状态。当用户按下 Back 按钮时,当前的activity将从堆栈顶部弹出(activity被销毁),之前的activity恢复(先前的UI状态被恢复)。栈中的activity从不会被重新排列,只会在被当前activity启动时被压到栈顶,然后在用户使用 Back 按钮时弹出。因此,返回栈是一个“last in, first out”的对象结构。图1用一个时间轴显示了activity与当前返回栈之间的进度。
图1 表示任务中每个新activity如何向返回栈添加项。当用户按下后退按钮时,当前的activity将被销毁,而之前的activity将恢复。
如果用户继续按 Back,那么堆栈中的每个activity都会被弹出,以显示前一个,直到用户返回到主屏幕(或者在任务开始时运行的任何activity)。当从堆栈中删除所有activity时,任务不再存在。
任务是一个内聚单元,当用户开始一个新任务或通过Home键进入主屏幕时,它可以移动到“后台”。在后台,任务中的所有activity都被停止,但是任务的返回栈仍然完整,任务只是在另一个任务产生时失去了焦点,如图2所示。然后,一个任务可以返回到“前台”,这样用户就可以在他们停止的地方继续工作了。例如,假设当前任务(任务A)在当前activity下的堆栈中有三个activity(两个activity在当前activity之下)。用户按下Home键,然后从应用程序启动器启动一个新的应用程序。当主屏幕出现时,任务A进入后台。当新应用程序启动时,系统就会为该应用程序启动一个有它自己的activity栈的任务(任务B)。在与该应用程序交互之后,用户返回Home屏并选择最初启动任务A的应用程序。现在,任务A进入前台,它的堆栈中的三个activity都是完整的,在堆栈顶部的activity恢复。此时,用户还可以通过 Home 并且选择启动该任务的app图标(或者从Recents屏幕中选择应用程序的任务)切换到任务B。这是Android上多任务处理的一个例子。
图2 两个任务:任务B在前台接收用户交互,而任务A在后台,等待恢复。
注意:多个任务可以同时在后台进行。但是,如果用户同时运行多个后台任务,系统可能会开始销毁后台activity以恢复内存,导致activity状态丢失。
因为返回栈中的activity不会重新排列,如果您的应用程序允许用户从多个activity中启动特定的activity,那么将创建一个新的activity实例并将其推送到堆栈上(而不是将activity的任何先前实例带到顶部)。因此,您的应用程序中的一个activity可能会被多次实例化(即使是来自不同的任务),如图3所示。因此,如果用户使用Back按钮向后导航,那么activity的每个实例将按照打开的顺序显示(每个实例都有自己的UI状态)。但是,如果不希望多次实例化activity,则可以修改此行为。在后面的部分中,我们将讨论如何 管理任务。
图3 单个activity实例化多次。
总结activity和任务的默认行为:
- 当activityA启动activityB时,activityA被停止,但是系统保留它的状态(例如滚动位置和表单中输入的文本)。如果用户在activityB中按下 Back 按钮,则activityA恢复其保存的状态。
- 当用户通过按Home键离开任务时,当前activity停止,任务进入后台。系统保留任务中的每个activity的状态。如果用户随后通过选择启动该任务的启动图标来恢复任务,任务就会出现在前台,并恢复在堆栈顶部的activity。
- 如果用户按下 Back 按钮,当前activity将从堆栈弹出并销毁。恢复堆栈中的前一个activity。当一个activity被销毁时,系统就不会保留activity的状态。
- activity可以多次实例化,甚至可以从其他任务实例化。
管理任务
如上所述,Android管理任务和返回栈的方式(所有activity陆续在同一的任务和“后进先出”栈中启动)适合于大部分的应用,你不用担心如何你的activity怎么和任务相联系或activity怎么存在于返回栈中。但是,您可能决定要中断正常的行为。也许您希望应用程序中的某个activity在启动时启动一个新任务(而不是放在当前任务中);或者,当您启动一个activity时,您希望将它的现有实例前移(而不是在返回栈上创建一个新实例);或者,您希望在用户离开任务时,除根activity之外的所有activity都从您的返回栈中清除。
您可以使用manifest中 <activity>
元素中的属性,和您传递给startActivity()的intent中的标志来达成这些。
在这方面,您可以使用的主要属性是:
taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch
你可以使用的主要intent标志是:
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_SINGLE_TOP
在下面的小节中,您将看到如何使用这些 manifest 属性和 intent 标志来定义activity与任务的关联以及它们在返回栈中的行为。
另外,还分别讨论了在Recents屏幕中如何表示和管理任务和activity的注意事项。有关更多信息,请参见 Recents屏幕。通常,您应该允许系统定义您的任务和activity如何在Recents屏幕中表示,您不需要修改此行为。
注意:大多数应用程序不应该中断activity和任务的默认行为。如果您确定您的activity需要修改默认行为,那么请谨慎使用,并确保测试在启动时和使用back按钮从其他activity和任务返回到它时activity的可用性。一定要测试导航行为,这些行为可能会与用户的预期发生冲突。
定义启动模式
启动模式允许您定义activity的新实例与当前任务关联的方式。你可以用两种方式定义不同的启动模式:
- 使用清单文件:当您在清单文件中声明某个activity时,您可以指定activity在启动时应该如何与任务关联。
- 使用intent标志:当您调用startActivity()时,您可以在intent中包含一个标志,该标志声明新activity是否应该与当前任务相关联。
因此,如果activityA开始activityB,activityB可以在它的清单中定义它应该如何与当前的任务相关联(如果有的话),activityA也可以请求activityB如何与当前任务关联的方式。如果这两个activity都定义了activityB应该如何与任务关联,那么activityA的请求(在intent中定义)将覆盖activityB的请求(在其清单中定义)。
注意:清单文件中的一些启动模式不能作为intent的标志,同样,一些用于intent的启动模式也不能在清单中定义。
使用清单文件
当在清单文件中声明activity时,可以指定activity使用 <activity>
元素的launchMode属性来关联任务的方式。
launchMode属性指定了如何将activity启动到任务中的指令。有四种不同的启动模式可以分配给launchMode属性:
"standard" (默认模式)
默认。系统在启动activity的任务中创建activity的一个新实例,并将intent路由到它。activity可以多次实例化,每个实例可以属于不同的任务,一个任务可以有多个实例。
"singleTop"
如果某个activity的实例已经存在于当前任务的顶部,则系统通过调用onNewIntent()方法将intent路由到该实例,而不是创建activity的新实例。可以多次实例化该activity,每个实例可以属于不同的任务,并且一个任务可以有多个实例(但只有在返回栈顶部的activity不是activity的现有实例时才可以)。
例如,假设一个任务的返回栈由根activityA,activityB、C和在顶端的D(堆栈是A -B-C-D;D在顶端)组成。一个intent启动D类型的activity。如果D有默认的“标准”启动模式,那么将启动一个新的类实例,并将堆栈变成A - B - C -D-D。但是,如果D的启动模式是“singleTop”,那么现有的D实例通过onNewIntent()来接收intent,因为它位于堆栈的顶部,堆栈仍然是A-B-C-D。然而,如果一个intent启动B类型的activity,那么即使它的启动模式是“singleTop”,也会将一个新的B实例添加到堆栈中。
注意:当创建activity的新实例时,用户可以按下 Back 按钮返回到以前的activity。但是,当一个activity的现有实例处理一个新的intent时,在新的intent到达onNewIntent()之前用户不能按后退按钮返回activity状态。
"singleTask"
系统创建一个新任务,并在新任务的根处实例化activity。但是,如果某个activity的实例已经存在于单独的任务中,则系统通过调用其onNewIntent()方法将intent路由到现有实例,而不是创建一个新实例。这个activity只能同时存在一个实例。
注意:虽然activity从一个新任务启动,但是后退按钮仍然会返回用户到之前的activity。
"singleInstance"
与“singleTask”相同,只是系统不会将任何其他activity启动到保存实例的任务中。activity始终是其任务的唯一成员;被这个activity启动的任何activity存在在单独的任务中。
另一个例子是,Android浏览器应用程序声明,web浏览器activity应该始终在自己的任务中打开——通过在 <activity>
元素中指定singleTask启动模式。这意味着,如果你的应用有intent打开Android浏览器,其activity不和你的应用程序放置在相同的任务中。相反,要么为浏览器启动一个新任务,要么如果浏览器已经有在后台运行的任务,这个任务会被前移去处理新的intent。
不管一个activity是在新任务中启动还是在启动它的activity的同一任务中启动, Back 按钮总是将用户带到以前的activity中。但是,如果您启动了一个activity,该activity指定了singleTask启动模式,那么如果该activity的实例存在于后台任务中,那么整个任务就会被带到前台。在这一点上,返回栈现在包括了被带到前台的任务中的所有activity,位于堆栈的顶部。图4说明了这种类型的场景。
图4 展示了如何将一个使用启动模式“singleTask”的activity添加到返回栈中。如果该activity已经是后台任务的一部分,并且此任务有它自己的返回栈,那么整个返回栈也会出现在当前任务的顶端。
注意:您使用launchMode属性为activity指定的行为可以被包含在启动您的activity的intent中的标志覆盖,如下一节所讨论的。
使用intent标志
当启动一个activity时,通过包含在您传递到startActivity()的intent中的标志,您可以修改activity和它的任务的默认关联。可以用来修改默认行为的标志是:
FLAG_ACTIVITY_NEW_TASK
在新任务中启动该activity。如果一个任务已经在运行您现在正在启动的activity,那么这个任务就会以其最后一个恢复的状态带到前台,并且该activity将在onNewIntent()中接收到新的intent。
这将产生与前一节讨论的“singleTask” launchMode值相同的行为。
FLAG_ACTIVITY_SINGLE_TOP
如果正在启动的activity是当前activity(位于返回栈的顶部),那么现有实例将接收到onNewIntent()的调用,而不是创建activity的新实例。
FLAG_ACTIVITY_CLEAR_TOP
如果正在启动的activity已经在当前任务中运行,那么将不再启动该activity的新实例,而是销毁它上面的所有其他activity,并通过onNewIntent()将此intent传递给activity的恢复实例(现在在顶部)。
没有launchMode属性任何值会产生这种行为。
FLAG_ACTIVITY_CLEAR_TOP通常与FLAG_ACTIVITY_NEW_TASK一起使用。在一起使用时,这些标志是一种定位另一个任务中现有activity并将其置于可以响应intent的位置的方法。
注意:如果指定activity的启动模式为“standard”,那么它也将从堆栈中移除,并在其位置启动一个新实例来处理传入的intent。这是因为当启动模式为“标准”时,总是为新的intent创建一个新的实例。
处理关联(affinities)
关联表示activity更喜欢属于哪个任务。默认情况下,来自同一应用程序的所有activity都具有相互关联。因此,默认情况下,同一应用程序中的所有activity都倾向于相同的任务。但是,您可以修改activity的默认关联。在不同应用程序中定义的activity可以共享一个关联,或者在同一个应用程序中定义的activity可以被分配到不同的任务关联中。
可以使用 <activity>
元素的taskAffinity属性来修改任何给定activity的关联。
taskAffinity属性接受一个字符串值,它必须和在 <manifest>
元素中声明的默认包名区分开来,因为系统使用该名称来标识应用程序的默认任务关联。
这种关联在两种情况下发挥作用:
- 当启动一个activity的intent包含了
FLAG_ACTIVITY_NEW_TASK
标志。
默认情况下,一个新的activity被启动到调用startActivity()的activity的任务中。它被推到与调用者相同的返回栈。然而,如果传递给startActivity()的intent包含了 FLAG_ACTIVITY_NEW_TASK
标志,系统将寻找一个不同的任务来容纳新的activity。通常,它是一项新任务。然而,这并不是必须这样。如果已经有一个与新activity具有相同关联的现有任务,则activity将被启动到该任务中。如果没有,它就会开始一个新的任务。
如果该标志导致activity开始一个新任务,用户按Home键离开它,则必须有某种方式让用户导航回到该任务。有些实体(例如通知管理器)总是在外部任务中启动activity,而从不作为它们自己的一部分,因此它们总是在它们传递给startActivity()的intent中放置了 FLAG_ACTIVITY_NEW_TASK
。如果你有一个activity,可以被可能会使用这个标志的一个外部实体调用 ,保证使用户有一个独立的方式回到已启动的任务,如启动器图标(任务的根activity有CATEGORY_LAUNCHER intent 过滤器;参见下面的启动任务部分)。
- 当一个activity设置它的
allowTaskReparenting
属性“true”时。
在这种情况下,当任务到达前台时,activity可以从它启动的任务转移到和一个它有关联的任务。
例如,假设某个报告选定城市天气状况的activity被定义为一个旅游应用程序的一部分。它与同一应用程序中的其他activity具有相同的关联(默认的应用程序关联),它允许对该属性进行重新定义归属。当您的一个activity启动天气预报activity时,它最初属于与您的activity相同的任务。然而,当旅行应用程序的任务到达前台时,天气报告activity将被重新分配到该任务并在其中显示。
提示:从用户的角度来说如果APK文件包含多个“应用”,那么您可能希望使用taskAffinity属性来为与每个“应用程序”相关的activity分配不同的关联。
清理返回栈
如果用户长时间离开任务,系统将清除该任务除根activity之外的所有activity。当用户再次返回任务时,只恢复根activity。系统的行为方式是这样的,是因为在长时间的之后,用户可能已经放弃了他们之前所做的事情,并返回到开始新工作的任务。
有一些activity属性可以用来修改这个行为:
alwaysRetainTaskState
如果这个属性在任务的根activity中被设置为“true”,那么刚才描述的默认行为不会发生。即使在很长一段时间之后,任务仍然保留其堆栈中的所有activity。
clearTaskOnLaunch
如果这个属性在任务的根activity中被设置为“true”,那么每当用户离开任务并返回时,堆栈就会被清除到根activity。换句话说,它与alwaysRetainTaskState
.相反。用户总是返回到任务的初始状态,即使在离开任务一段时间后也是如此。
finishOnTaskLaunch
这个属性类似于clearTaskOnLaunch,但它只在单个activity上运行,而不是整个任务。它也可以导致任何activity消失,包括根activity。当它被设置为“true”时,该activity仅为当前会话保留一部分任务。如果用户离开,然后返回任务,那么它就不再存在了。
开始一个任务
您可以通过给activity一个以“android.intent.action.MAIN”指定行动和以“android.intent.category.LAUNCHER”指定类别的intent过滤器来设置一个任务的入口点。例如:
<activity ... >
<intent-filter ... >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...
</activity>
这种类型的intent过滤器会在app启动器中显示activity的图标和标签,让用户可以启动activity,并返回它在任何时间启动后创建的任务。
第二种能力很重要:用户必须能够离开任务,然后使用这个activity启动器返回到它。因此,作为activity启动任务的两种模式 "singleTask"
和"singleInstance",只有当activity具有 ACTION_MAIN
和 CATEGORY_LAUNCHER
过滤器时,才应该使用。例如,想象一下,如果过滤器缺失了会发生什么:一个intent启动一个"singleTask" activity,启动一个新任务,用户花费一些时间在这个任务上。然后用户按Home键。任务现在被发送到后台,并且不可见。现在,用户无法返回任务,因为它没有在应用程序启动器中出现。
对于那些您不想让用户返回到某个activity的情况,将<activity>元素的finishOnTaskLaunch
设置为“true”。
关于任务和activity如何在Overview界面中表示和管理的进一步信息,可以在 Recents屏幕 中找到。