Android开发-API指南-任务和回退栈
Task and Back Stack
英文原文: http://developer.android.com/guide/components/tasks-and-back-stack.html
采集(更新)日期:2014-12-16
一个应用程序通常包含多个 Activity。 每个 Activity 都应该围绕一类 Action 来进行设计,这个 Action 可供用户执行并能启动其他 Activity。 比如,一个 email 应用可以用一个 Activity 列出所有新邮件,当用户选中其中一封时,再打开一个新的 Activity 来显示详情。
Activity 甚至可以启动当前设备上另一个应用中的 Activity。 比如,如果当前应用需要发送 email,可以定义一个 Intent 来执行“send” action,此 Intent 中可以包含一些数据,如 email 地址、正文等。 然后,会打开一个已声明能够处理这类 Intent 的属于其他应用的 Activity。 现在是一个发送 email 的 Intent,所以会打开一个 email 应用的“新建邮件”(compose) Activity(如果同时有多个 Activity 支持该类 Intent,则系统会让用户选用其中一个)。 当 email 发送完毕后,将会恢复之前的 Activity ,看起来 email Activity 就像是当前应用中的一部分一样。 虽然多个 Activity 可能来自不同的应用,通过把他们放入同一个Task,Android 系统可以提供连贯无缝的用户体验。
任务(Task)是多个 Activity 的集合,用户进行操作时将与这些 Activity 进行交互。 这些 Activity 按照启动顺序依次被存入“回退”栈(“Back Stack”)中。
大部分 Task 都启动自主屏幕(Home)。当用户点击应用程序列表(Application Launcher)中的图标(或主屏幕上的快捷图标)时,应用程序的 Task 就进入前台。 如果该应用的 Task 不存在(最近没有使用过该应用),则会新建一个任务,该应用的主(“main”)Activity 将作为回退栈的根 Activity 被打开。
如果当前 Activity 启动了另一个 Activity,则新的 Activity 将被压入栈顶并获得焦点。 前一个 Activity 仍然保存在栈中,但是处于停止状态(Stop)。在 Activity 处于停止状态时,系统会保存用户界面的当前状态。 当用户按下回退键时,当前 Activity 将从栈顶弹出(被销毁),前一个 Activity 将被恢复(resume)(之前的用户界面状态同时被还原)。 Activity 在回退栈中的顺序永远不会改变,只能压入(push)和弹出(pop)——被当前 Activity 启动时压入栈顶,用户用回退键离开时弹出。 这样,回退栈 以“后进先出”的方式运行。图1 以时间线的形式展示了多个 Activity 切换时各时间点的回退栈状态。
如果用户连续不停地按下回退键,则回退栈中所有 Activity 都会被依次弹出并显示出来,直至用户回到主屏幕(或是启动本任务的 Activity)。 当所有 Activity 都从栈中弹出后,本任务就此消失。
任务(Task)是一种整体单位, 当用户启动一个新任务或回到主屏幕时,当前任务就可以转到“后台”。 当任务处于后台时,其中所有的 Activity 都处于停止状态,但是这个任务的回退栈仍然完整保留——如图2所示,在其他任务获得焦点期间,该任务只是失去焦点而已。 任务可以再次返回“前台”,所以用户能够重新回到刚才离开的状态继续工作。 比如,假定当前任务(A任务)的回退栈中共有3个 Activity —— 下面还有两个 Activity。 这时,用户按下Home键,并从应用程序列表中启动了一个新的应用。 当主屏幕出现时,A任务进入后台。 当新的应用启动时,系统会为它再开一个任务 (B任务),新应用中的 Activity 将存放其中。 新应用使用完毕后,用户再次返回主屏幕,并选择执行那个启动A任务的应用。 现在,A任务回到前台——栈中的三个 Activity 仍然完好,位于栈顶的 Activity 恢复运行。 这时,用户仍然可以切回B任务,通过返回主屏幕并选择执行相应的应用图标即可,或者在最近任务列表(overview screen中选择相应任务)。 以上便是 Android 多任务的实例。
因为回退栈中的 Activity 顺序永远不会改变,如果某个 Activity 允许被用户启动多个实例,则新创建的实例会压入栈顶(而不是打开已位于栈顶的 Activity 实例)。 于是,一个 Activity 可能会被初始化多次(甚至会位于不同的任务中),如图3所示。 这时,如果用户用回退键回退,则 Activity 的每个实例都会沿着启动顺序显示回去(包括用户界面的原有状态)。 当然,如果不想让 Activity 被多次实例化,可以修改这种方式。修改的方法将在后续章节 管理多个任务 中讨论。
下面把 Activity 和任务的默认特性汇总一下:
- 当 Activity A 启动 Activity B 时,Activity A 将被停止,但系统仍会保存其状态(如滚动条位置和表单中录入的文字)。 如果用户在 Activity B 中按下回退键,则 Activity A 恢复运行,状态也将被恢复。
- 当用户按下Home键离开任务时,当前 Activity 被停止,当前任务转入后台。 系统会保存任务中每个 Activity 的状态。如果用户将来通过启动应用图标来恢复任务,那么该任务就会回到前台,栈顶的 Activity 会恢复运行。
- 如果用户按下回退键,当前 Activity 将从回退栈中弹出并被销毁。 栈中的前一个 Activity 将恢复运行。 当 Activity 被销毁后,系统不会保留其状态。
- Activity 可以被多次实例化,甚至可以位于不同的任务中。
导航设计
关于 Android 各应用程序间跳转的工作机制,请参阅 Android 导航 设计指南。
保存 Activity 状态
如上所述,系统默认会在 Activity 停止时保存其状态。这样,当用户返回时,用户的界面就能保持与离开时的一样。 不过,你可以——也应该——用回调方法来主动地保存 Activity 的状态,以便应对 Activity 被销毁并重建的情况。
当停止 Activity 时(比如启动了一个新 Activity 或者任务转入后台),出于腾出内存的需要,系统也许会完全销毁该 Activity。 这样该 Activity 的状态信息将会丢失。如果发生这种情况,系统仍然是记得该 Activity 已经被置入回退栈了。 但是当它位于栈顶时,系统必须重建它(而不是恢复)。为了防止用户工作内容的丢失,你应该主动保存这些内容,通过实现 Activity 的 onSaveInstanceState() 回调方法即可。
关于如何保存 Activity 状态的详情,请参阅文档 Activities。
多任务管理
如上所述—— Android 把所有已启动的 Activity 相继放入同一个任务中以及一个“后入先出”栈。 这种管理任务和回退栈的方式适用于大多数应用, 不必去操心各 Activity 如何与任务关联,以及如何弹出回退栈。 不过,有时也许有必要改变这种通常的运行方式。 也许某个 Activity 需要启动一个新的任务 (而不是被放入当前任务); 或者,启动 Activity 时需要调出已有的某个实例(而不是在回退栈顶创建一个新的实例); 或者,当用户离开任务时只需要保留根 Activity,而回退栈中的其他 Activity 都要被清掉。
不仅如此,利用 <activity> manifest 元素的属性和传入 startActivity() 的 Intent 中的标志位属性,还可实现更多需求。
这里可以使用的 <activity> 属性主要包括:
- taskAffinity
- launchMode
- allowTaskReparenting
- clearTaskOnLaunch
- alwaysRetainTaskState
- finishOnTaskLaunch
可用的 intent 标志主要有:
下一节介绍了如何利用这些 manifest 属性和 Intent 标志位属性来定义 Activity 与任务的关联性,以及他们在回退栈中的工作方式。
并且,后续还单独讨论了任务和 Activity 如何在最近任务列表中分别表示和管理的。 更多信息请参阅 Overview Screen。 通常,应该让系统来决定任务和 Activity 在最近任务列表中的表示方式,不需要去改动它。
定义启动模式
启动模式定义了 Activity 新实例与当前任务的关联方式。定义启动模式的方法有两种:
- 使用 manifest 文件
在 manifest 文件中声明一个 Activity 时,可以指定启动它时与任务的关联方式。
- 使用 Intent 中的标志位属性
调用 startActivity() 时,可以在 Intent 中包含一个标志,用于指明新 Activity 如何(是否)与当前任务相关联。
这样,如果 Activity A 启动了 Activity B,则 Activity B 可以在 manifest 中定义它如何与当前任务关联(如果存在的话), 并且,Activity A 也可以指定 Activity B 与当前任务的关联关系。 如果两者都定义了,则 Activity A 的请求(Intent 中定义)优先于 Activity B 的定义(在 manifest 文件中)。
使用 manifest 文件
在 manifest 文件中声明 Activity 时,你可以利用 <activity> 元素的 launchMode 属性来设定 Activity 与任务的关系。
launchMode 属性指明了 Activity 启动任务的方式。 launchMode 属性可设为四种启动模式:
- "standard" (默认模式)
- 默认值。系统在启动 Activity 的任务中创建一个新的 Activity 实例,并把 Intent 传递路径指向它。 该 Activity 可以被实例化多次,各个实例可以属于不同的任务,同一个任务中也可以存在多个实例。
- "singleTop"
- 如果 Activity 已经存在一个实例并位于当前任务的栈顶,则系统会调用已有实例的 onNewIntent() 方法把 Intent 传递给已有实例,而不是创建一个新的 Activity 实例。 Activity 可以被实例化多次,各个实例可以属于不同的任务,同一个任务中可以存在多个实例(但仅当回退栈顶的 Activity 实例不是该 Activity 的)。
比如,假定某个任务的回退栈中包含了根 Activity A 和 Activity B、C、D(顺序是 A-B-C-D;D 在栈顶)。 这时,传来了一个启动 Activity D 的 Intent。 如果 Activity D 的启动模式是默认的"standard",则会启动 Activity D 类的一个新实例,栈的内容变为 A-B-C-D-D。 但是,如果 Activity D 的启动模式是"singleTop",则已有的 Activity D 实例会通过 onNewIntent() 接收这个 Intent,因为该实例位于栈顶——栈中内容仍然维持 A-B-C-D 不变。 当然,如果此 Intent 是要启动 Activity B 的,则 Activity B 的一个新实例还是会被加入栈中,即使 Activity B 的启动模式是"singleTop"也是如此。
注意: 一个 Activity 的新实例创建完毕后,用户可以按回退键返回前一个 Activity。 但是如果是由 Activity 的已有实例来处理刚到达的 Intent ,那用户就无法用回退键回到 onNewIntent() 中 Intent 到来之前的 Activity 状态了。 - "singleTask"
- 系统将创建一个新的任务,并把 Activity 实例作为根成员放入其中。 但是,如果该 Activity 已经在其他任务中存在实例了,则系统会调用已有实例的 onNewIntent() 方法把 Intent 传给已有实例,而不是再创建一个新实例。 本类 Activity 同一时刻只能存在一个实例。
注意: 虽然 Activity 启动了一个新的任务,但用户仍然可以用回退键返回前一个 Activity。
- "singleInstance"
- 除了系统不会把其他 Activity 放入当前实例所在的任务之外,其他均与"singleTask"相同。 Activity 总是它所在任务的唯一成员;它所启动的任何 Activity 都会放入其他任务中。
再举个例子,Android 的浏览器应用就把 web 浏览器 Activity 声明为总是在它自己独立的任务中打开——把 <activity> 设为singleTask模式。 这意味着,如果某个应用提交 Intent 来打开 Android 的浏览器,则其 Activity 不会被放入该应用所在的任务中。 而是或为浏览器启动一个新的任务;或者浏览器已经在后台运行,只要把任务调入前台处理新的 Intent 即可。
无论 Activity 是在一个新的任务中启动,还是位于其他已有的任务中,用户总是可以用回退键返回到前一个 Activity 中。 但是,如果启动了一个启动模式设为singleTask的 Activity,且有一个后台任务中已存在其实例的话,则这个后台任务就会整体切换到前台。 这时,当前的回退栈中就包含了这个转入前台的任务中的全部 Activity,且位置是在栈顶。 图 4 就展示了这种场景。
关于在 manifest 文件中使用启动模式的详情,请参阅 <activity> 元素文档,其中详细描述了launchMode属性及其可用值。
使用 Intent 标志位属性
在启动 Activity 时,可以在传给 startActivity() 的 Intent 中包含相应标志位,以便修改 Activity 与任务的默认关系。 可用于修改默认启动模式的标志位包括:
FLAG_ACTIVITY_NEW_TASK在新的任务中启动 Activity。 如果要启动的 Activity 已经在某任务中运行,则那个任务将被调入前台,最后保存的状态也将恢复,Activity 将在 onNewIntent() 方法中接收到这个新 Intent。这个过程与上一节的"singleTask" 启动模式相同。
FLAG_ACTIVITY_SINGLE_TOP如果要启动的 Activity 就是当前 Activity(位于回退栈 顶),则已存在的实例将接收到一个 onNewIntent() 调用,而不是创建一个 Activity 的新实例。这个过程与上一节的 "singleTop" 启动模式相同。
FLAG_ACTIVITY_CLEAR_TOP如果要启动的 Activity 已经在当前任务中运行,则不再启动一个新的实例,且所有在其上面的 Activity 将被销毁, 然后通过 onNewIntent() 传入 Intent 并恢复 Activity(不在栈顶)的运行,此种模式在launchMode 属性中没有对应值。
FLAG_ACTIVITY_CLEAR_TOP 常与 FLAG_ACTIVITY_NEW_TASK 一起使用。 这表示先找到其他任务中已存在的 Activity,再把此 Activity 放入可以响应 Intent 的位置。
处理 affinity
affinity 表示 Activity 期望被放入的任务。 缺省情况下,同 表示 Activity 期望被放入的任务。 缺省情况下,同一个应用中的所有 Activity 都拥有同一个 affinity 值。 因此,同一个应用中的所有 Activity 默认都期望位于同一个任务中。 不过,你可以修改 Activity 默认的 affinity 值。 不同应用中的 Activity 可以共享同一个 affinity 值,同一个应用中的 Activity 也可以赋予不同的任务 affinity 值。
你可以用<activity> 元素的 taskAffinity 属性修改 activity 的 affinity,
taskAffinity 属性是一个字符串值,必须唯一,并与 <manifest> 元素定义的默认包名称有所区别,因为系统用这个包名称作为应用的默认任务的 affinity 值。
affinity 将在以下两种情况下发挥作用:
- 启动 Activity 的 Intent 中包含了 FLAG_ACTIVITY_NEW_TASK。
默认情况下,一个新的 Activity 将被启动并放入调用 startActivity() 的 Activity 所在的任务中,且压入调用者所处的回退栈顶部。 不过,如果传给 startActivity() 的 Intent 包含了 FLAG_ACTIVITY_NEW_TASK 标志,则系统会查找另一个任务并将新 Activity 放入其中。 这时常常会新开一个任务,但并非一定如此。 如果一个已有任务的 affinity 值与新 Activity 的相同,则 Activity 会放入该任务中。 如果没有,则会新建一个任务。
如果本标志使得 Activity 启动了一个新的任务,则当用户按下 Home 键离开时,必须采取一些措施以便用户能回到此任务。 某些应用(比如通知管理器)总是让 Activity 放入其它任务中启动,而不是放入自己的任务中。 因此,它们总是把 FLAG_ACTIVITY_NEW_TASK> 标志放入传给startActivity() 的 Intent 中。 如果你的 Activity 可以被外部应用用此标志来启动,请注意用户会用相对独立的方式返回启动时的任务, 比如通过启动图标(这类任务的根 Activity 带有一个 CATEGORY_LAUNCHER Intent 过滤器;参阅下节 启动任务)。
- 当一个 Activity 的 allowTaskReparentingl设置为"true"。
这种情况下,当某个任务进入前台时,而此 Activity 的 affinity 值又与其相同,则该 Activity 可以从启动时的任务移入这个任务中。
比如,假定某旅游应用中含有一个根据所选城市预报天气的 Activity。 它的 affinity 与同一应用中的其他 Activity 一样(整个应用默认的 affinity),且它允许通过本属性重新指定所属任务。 当其他应用的 Activity 启动此天气预报 Activity 时,它将位于此应用的任务中。 然而,当旅游应用的任务进入前台时,天气预报 Activity 将会重新回到其任务中并在其中显示。
清理回退栈
如果用户离开某个任务已有很长时间了,系统将会仅保留一个根 Activity,而把其他 Activity 都清除掉。 当用户返回该任务时,只有根 Activity 会被恢复。 系统之所以这么处理,是因为经过了很长时间后,用户应该是要放弃之前进行的工作,而返回任务是为了开始新的工作。
你可以利用 Activity 的某些属性来改变这种处理方式:
- alwaysRetainTaskState
- 如果任务中的根 Activity 的此属性设为 "true" ,则默认的清理方式不会进行。 即使过了很长的时间,任务中所有的 Activity 也都会被保留在栈中。
- clearTaskOnLaunch
- 如果任务中根 Activity 的此属性设为 "true",则只要用户离开并再次返回该 task,栈就会被清理至根 activity。 也就是说,正好与 alwaysRetainTaskState 相反。用户每次返回任务时看到的都是初始状态,即使只是离开一会儿。
- finishOnTaskLaunch
- 此属性类似于 clearTaskOnLaunch ,只是它只对某一个 Activity 生效,而不是整个任务。 这可以使得任何一个 Activity 消失,包括 根 Activity。 如果 Activity 的此属性设为 "true",则只会保留 task 中当前 session 所涉及的内容。 如果用户离开后再返回任务,它就不存在了。
启动任务
你可以把某个 Activity 设为任务的入口,通过发送一个 Action 为 "android.intent.action.MAIN"、category 为 "android.intent.category.LAUNCHER" 的 Intent 即可。 比如:
<activity ... >
<intent-filter ... >
<actionandroid:name="android.intent.action.MAIN"/>
<categoryandroid:name="android.intent.category.LAUNCHER"/>
</intent-filter>
...
</activity>
这种 Intent 过滤器将会把此 Activity 的图标和文本标签(label)作为应用程序的启动图标来显示, 用户可以启动此 Activity,并且在之后任何时候返回创建时的任务。
第二种特性非常重要:用户必须能够离开一个任务,之后还能再回来使用这个启动任务的 Activity。 因此,标明 Activity 每次都会启动任务的这两种 启动模式: "singleTask" 和 ""singleInstance" 应该仅对带有 ACTION_MAIN 和 CATEGORY_LAUNCHER 过滤器的 Activity 才能使用。 想象一下,如果未给出这类过滤器会发生什么: 某个 Intent 启动了一个 "singleTask" 模式的 Activity, 并新建了一个任务,用户在此任务中工作了一段时间。 然后他按下了 Home 键。 当前任务就转入后台,变为不可见状态。这时用户就无法再回到该任务了,因为在应用程序列表(Application Launcher)中没有相应的项目显示。
对于那些不想让用户返回的 Activity,把 <activity> 元素的 finishOnTaskLaunch 设为 "true" 即可(参阅 清理回退栈。
关于在最近应用列表中如何显示并管理任务和 Activity 的更多信息,请参阅 Overview Screen。