[译]Tasks and Back Stack

Tasks and Back Stack

一个Application通常包括多个Activity。每一个Activity都应该被设计为执行用户特定行为和启动其他Activity。比如,一个Email Application可能拥有一个列举Email的Activity,当用户选择一封Email时,启动一个新的Activity来阅读这封Email。

一个Activity甚至可以启动device中其他Application存在的Activity。比如,如果你的Application想要发送Email,你可以定义一个执行Send Action并且包含一些数据(Email地址和Message)的Intent。其他应用中可以处理此类Intent的Activity会打开。在这个例子里,Intent是send Email,所以Email Application的编辑Activity会启动(当有多个Activity可以处理该Intent时,会给出选择供用户选择)。当Email发送完成后会返回resume到你的Activity,看起来Email Activity像是你的Application的一部分,即使这些Activity是在不同的Application。Android通过保持这个两个Activity在同一个Task中来提供无缝的用户体验。

Task是用户执行某个特定工作时交互的Activity的集合。这些Activity按照他们打开的顺序排列在一个Stack中(Back Stack)。

Device的主界面(Home Screen)是大多数Task的起点。当用户点击Application的启动图标(也可能是快捷方式)时,此Application的Task会切换到前台(foreground)。如果此Application还没有Task存在(此Application最近没有被启动过),会有一个新的Task生成,这个Application的main Activity会被作为这个Stack的根Activity打开。

当当前Activity启动另外一个时,新Activity被push到栈顶并且获得焦点。前一个Activity保留在Stack中,但是Stopped。当一个Activity Stopped,系统会保留它的UI的当前状态。当用户点击Back按钮,当前Activity会从Stack中弹出(并且Destroy),前一个Activity resume(UI状态会恢复)。Stack中的Activity永远不会被重新排列,只会在Stack中push和pop。被当前Activity启动时push到栈顶,用户点击Back离开Activity时pop出栈。

此Back Stack做为一种“LIFO”(后进先出)的对象结构。下图展示了这种进出关系:

diagram_backstack

如果用户不断的点击Back按钮,Activity会Pop出栈而现实它之前的一个,知道用户回到Home Screen(或者回到这个Task开始时正在运行的Activity)。当所有Activity都从这个Stack移除时,这个Task就不存在了。
当用户启动一个新的Task或回到Home Screen时,Task可以作为一个凝聚的单位整体切换到后台background。在后台,Task中的所有Activity都Stopped了,但是这个Task的Stack不变——-只是另一个Task执行时此Task失去了focus,如图所示:

diagram_multitasking

图解:Task B由于用户交互切换到前台,Task切换到后台等待resume。

一个Task可以切换到foreground,所有用户可以在任何他们离开的地方再次开始。举个例子:Task A在它的Stack中有三个Activity(两个在当前Activity之下)。用户点击Home按钮,然后启动另外一个Application,系统会启动一个新的拥有自己的Stack的Task(Task B)。与此交互完之后,用户再次回到Home启动Task A的Application。现在,Task A处于foreground(其Stack中的三个Activity不变,且栈顶的Activity Resumed)。关键是,用户可以通过Home再次回到Task B。这就是Android中多任务处理的实例。

注意:多个任务可以同时存在于后台background。然而,当用户同时在后台运行了过多的任务是,系统可能为了回收内存而销毁一些后台Activity,导致Activity的状态丢失。参见以下Activity State章节。

因为Back Stack中Activity绝对不会被重排,如果你的Application中允许用户通过多个Activity去启动一个特定的Activity,此Activity的一个新实例被创建并push到栈顶(而不是将该Activity之前的实例至于栈顶)。这样,你的Application的一个Activity可能被实例化多次(甚至可能在不同的Task),如图所示:

diagram_multiple_instances

图解:HomeActivity被实例化多次。

这样,当用户使用Back键向后导航时,每个实例根据打开的顺序都会被显示(每个都有自己的UI状态)。当然,如果你不想Activity被实例化多次,你可以更改这种行为。具体怎么做会在Managing Tasks章节讨论。

总结下Activity和Task的默认行为:

  • 当Activity A启动Activity B,A stopped,但是系统会保存其状态(比如滚动的位置和表单中输入的text)。当用户在B中按Back键,A会恢复状态并resume。
  • 当用户点击Home键退出一个Task时,当前Activity会Stop且它所在的Task切入后台background。系统会保持Task中所有Activity的状态。当用户之后resume此Task时,此Task切换到前台foreground且Task的Stack中栈顶的Activity reusme。
  • 当用户点击Back键时,当前Activity会pop出栈并销毁。Stack中前一个Activity resume。当一个Activity被销毁时,系统不会保存其状态。
  • Activity可以被实例化多次,即使是在不同的Task中。

Saving Activity State

如上讨论,系统默认在Activity Stopped时保存其状态。这么说,当用户通过Back键回到之前的Activity时,展示的UI和他离开时是一样的。但是,你可以或者说应该,主动的调用lifecycle的回调保存Activity的状态,来避免万一Activity被销毁而必须重建。

当系统Stop你的Activity(启了一个新的Activity或是Task移到后台),系统都可能在它回收系统内存时将此Activity完全销毁。此时,Activity的状态信息都将丢失,系统仍然知道此Activity在Back Stack中的位置,但是当此Activity回到前台时系统会重建它(而不是resume)。为了避免丢失用户工作,你应该在你的Activity中实现onSaveInstanceState方法积极的保存状态。

更多Activity状态信息,参见Activity文档

Managing Tasks

Android管理Task和Back Stack的方式,如上讨论:将连续的Activity置于一个Task和一个Stack中,大多数Application都可以很好的运作而且你不用担心你的Activity如何与Task关联,如何存在于Back Stack中。

然后,你可能想要中断一些正常行为。也许你想你的Application中的一个Activity单独启动一个新的Task;或者当你启动一个Activity时,你希望使用之前创建的实例而不是重新在Back Stack建一个新的实例;又或者你希望用户离开Task时清除你的Back Stack除了根Activity。

元素中使用属性或是startActivity的Intent中添加flag,你可以做的更多。

有如下属性可以使用:

  • taskAffinity
  • launchMode
  • allowTaskReparenting
  • clearTaskOnLaunch
  • alwaysRetainTaskState
  • finishOnTaskLaunch

主要有以下flag可以使用:

  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_SINGLE_TOP

在以下的章节,你将看到如何使用这些属性和flag去定义Activity关联Task和Back Stack。

注意:大多数Application不需要打断Activity和Task的默认行为。如果你认为改变你Activity的默认行为是必要的,请谨慎使用,且测试Activity在运行时以及通过Back按钮从别的Activity或Task返回时的可用性。一定要测试可能与用户体验冲突的导航行为。

Defining launch modes

Launch Mode让你可以定义你的Activity新实例与当前Task的关联关系。你可以通过两种方式定义Launch Mode:

  • 使用Manifest文件
  • 使用Intent Flag

当Activity A启动Activity B,B可以在manifest文件中定义它与当前Task的关系,A也可以请求B与当前Task的关系。如果两种方式都定义了,则A的请求(Intent)覆盖B的请求(manifest)。

注意:一些Launch mode作用于manifest而不作用于intent flag,同样的一些Launch mode对intent flag有效而不能在manifest中定义。

Using the manifest file

当我们在manifest文件中声明Activity时,我们可以使用的launchMode属性来指定我们的Activity应该如何关联一个Task。

launchMode属性指明你的Activity如何在一个Task中启动。有四种属性值:

  • Standard
    默认值。当Activity启动时系统会在Task中创建一个该Activity的实例。此类Activity可以被实例化多次,每个实例可以属于不同的Task,且一个Task可以拥有它的多个实例。

  • singleTop
    如果已经有一个该Activity的实例处于当前Task的栈顶了,系统会通过调用onNewIntent方法引导Intent去调用该实例,而不是为此Activity创建一个新的实例。此类Activity可以被实例化多次,每个实例可以属于不同的Task,且一个Task可以拥有它的多个实例(但是当栈顶是该Activity的现有实例,不会创建新的实例)。

例如:假设一个Task的Back Stack里面有A、B、C、D四个Activity,A-B-C-D(D在栈顶)。一个Intent要启动D,如果D是standard模式的话,会创建一个新的D实例,Stack变为A-B-C-D-D。然而,如果D是singeTop模式的话,已存在的D实例会通过onNewIntent方法来接收这个Intent,以为D已经在栈顶了,此时Stack依然是A-B-C-D。但是,如果来了一个启动B的Intent,Stack中会新建一个B的实例而不管B的launchMode是不是singleTop。

注意:当Activity被创建一个新的实例时,用户可以通过Back键回到前一个Activity。当时当一个当前Activity实例处理一个新的Intent时,用户不可以通过Back回到这个Activity的onNewIntent之前的状态。

  • singleTask
    系统会创建一个新的Task,且在这个新的Task的root实例化此Activity。然而,当已经有一个该Activity的实例存在于一个单独的Task时,系统会通过调用onNewIntent方法引导Intent传向已有的这个Activity实例,而不是创建一个新的实例。同一时间只能存在一个Activity实例。

注意:虽然此Activity运行在一个新的Task中,用户点击Back键依然可以回到前一个Activity。

  • singleInstance
    与singleTask类似,除了系统在该Activity所在的Task中不会再启动其他任何Activity。Task中有且仅有一个此Activity,任何该Activity启动的Activity都会在另外的Task中打开。

另外一个例子:Android Browser应用声明网页浏览Activity应该总是在自己的Task中打开(在元素中定义singleTask launch mode)。这意味着如果你的Application发送一个Intent去打开Browser,它的Activity不会置于你的Application的Task中。系统会为Browser启动一个新的Task(或者如果已经有一个Browser的task运行在后台了,该Task会切换到前台来处理这个Intent)。

不管一个Activity是运行在一个新的Task还是原来的Task(启动这个Activity的Task)中,用户点击Back键总是可以回到上一个Activity。但是,如果你以singleTask的方式启动一个Activity,且如果已经有一个该Activity的实例存在于后台Task,此Task会整个切换到前台。此时,此Task的Back Stack中的所有Activity都会切换回来处于Stack的顶部。下图展示了此种场景:

diagram_backstack_singletask_multiactivity

图解:Activity Y以singleTask的方式运行于后台Task中,此时Activity2启动Y,会将Y所在的Task的整个Back Stack带回前台。用户按Back键会从Y所在的Task开始回退。

注意:你在manifest中定义的Activity的launch mode属性可以被启动它的Intent flag所覆盖。如下面讨论。

Using Intent flags

当你启动一个Activity时,你可以在启动的Intent加上flag来指明Activity关联的Task。有以下几种flag:

  • FLAG_ACTIVITY_NEW_TASK
    在新的Task中启动Activity。如果已经有一个Task运行着你要启动的Activity,此Task会恢复其最后的状态并切换到前台,而此Activity会在onNewIntent中接收到Intent。
    与“singleTask”的行为一样。

  • FLAG_ACTIVITY_SINGLE_TOP
    当你要启动的Activity正是当前Activity(已经处于栈顶),此已存在的Activity实例会接收Intent并调用onNewIntent,而不是重新创建一个Activity实例。
    与“singleTop”行为一致。

  • FLAG_ACTIVITY_CLEAR_TOP
    当你要启动的Activity之前已经在当前Task运行,不会创建一个新的Activity实例,而是将Stack中该Activity实例之上的Activity全部销毁(此时该实例位于栈顶),该Activity实例resume调用onNewIntent处理Intent。
    没有相应的launch mode与此flag对应。
    FLAG_ACTIVITY_CLEAR_TOP通常与FLAG_ACTIVITY_NEW_TASK一起使用。当他们一起使用时,共同目标是把一个已经存在的Activity放入另一个Task且放在一个可以响应Intent的位置。

注意:如果一个Activity的launch mode指定为“standard”,当接收到新的Intent时,它也会被从Stack中移除而新启一个新实例在它的位置去处理该Intent。这是因为launch mode为“standard”的Activity总是会为Intent新建一个实例。

Handling affinities(亲合力)

Affinity指明Activity更倾向于属于哪个Task。默认情况下,同一个Application的Activity彼此拥有一个Affinity。所以,默认同一个Application的Activity运行于同一个Task。但是,你可以改变一个Activity的默认Affinity。不同Application的Activity可以共享一个Affinity,或者同一个Application的Activity可以被赋予不同的Task Affinity。

你可以通过元素的taskAffinity属性来改变Activity的Affinity。

taskAffinity是一个字符串值,它必须来自定义与manifest中的唯一包名(package name)。因为系统会使用该名字去区分Application的task affinity。

在以下两种情况Affinity发挥它的作用:

  • 启动Activity的Intent带有FLAG_ACTIVITY_NEW_TASK
    默认情况下,startActivity启动的新Activity会与它的调用者处于同一个Task,被push到同一个Back Stack。但是,如果startActivity的Intent带有FLAG_ACTIVITY_NEW_TASK,系统会寻找一个不同的Task来容纳新的Activity。通常,这是一个新的Task。然而也不是必须的,如果已经存在着一个与新Activity相同Affinity的Task,新Activity会在此Task中运行。如果没有,才会产生一个新的Task。
    如果此Flag导致Activity运行在一个新的Task,用户点击Home键离开时,应该有一些方式让用户返回到此Task。
    一些实体(比如Notification Manager)总是在另外的Task中启动Activity,而非他们本身,所以他们总是使用带有FLAG_ACTIVITY_NEW_TASK的intent来startActivity。
    如果你有一个Activity可以被其他的实体通过此flag方式调用,注意用户有一个独立的方式Back到原来的Task。比如通过Launcher icon(Task的根Activity具有CATEGORY_LAUNCHER的Intent filter,参见Staring a Task章节)。

  • Activity的allowTaskReparenting的属性值为true
    在这种情况下,一个Activity可以从启动它的Task移动到和它拥有同一个Affinity的Task(当此Task切换到前台时)。
    比如:假设一个旅游Application定义了一个预报选定城市的天气状况的Activity。它与此Application中的其他Activity具有相同的Affinity(默认的Application Affinity),且其allowTaskReparenting属性为true。当你的Activity启动此天气预报Activity时,它会被初始化与你的Activity相同的Task。但是,当此旅游Application的Task切换到前台时,此天气预报的Activity会重新分配到这个Task,且在此Task中显示。

注意:从用户角度看,如果一个apk包含了多个“Application”,你可能要使用“taskAffinity”属性来给属于不同“Application”的Activity分配不同的Affinity值。

Clearing the back stack

如果用户离开一个Task很长时间,系统会清除这个Task中Activity除了root Activity。当用户在此回到此Task时,只有此root Activity会被恢复。系统如此设计是因为经过很长时间之后,用户可能已经放弃了之前所做的,重新回来是想做新的事情。

有一些Activity属性可以用来修改此默认行为。

  • alwaysRetainTaskState
    如果Task中的root Activity的此属性设置为true,上述默认行为不会发生。此Task会保留Stack中的所有Activity,即使经过了很长时间。

  • clearTaskOnLaunch
    如果Task中的root Activity的此属性设置为true,无论什么时候用户离开此Task后再返回它,此Stack都会清除只剩下root Activity。换句话说,此属性是alwaysRetainTaskState的对立面,用户总是以初始状态回到此Task,即使只是离开了一瞬间。

  • finishOnTaskLaunch
    此属性类似于clearTaskOnLaunch,但是它仅作用与单独的Activity而不是整个Task。它会导致任意一个Activity出栈,包括root Activity。当它设置为true,此Activity仅仅会为当前会话作为Task的一部分保留。当用户离开并重新返回该Task时,此Activity不再存在。

Starting a task

你可以设定一个Activity作为一个Task的入口,通过给他的Intent filter一个“android.intent.action.MAIN”的action和一个“android.intent.category_LAUNCHER”的category。如:

1
2
3
4
5
6
7
<activity ... >
<intent-filter ... >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...
</activity>

拥有此intent filter的Activity会在Launcher Application中显示其icon和label,给用户一种方式去启动此Activity或是在其已经被创建的情况回到创建它的Task。

第二个能力相当重要:用户必须可以在离开一个Task后通过Launcher重新回来。为此,“singleTask”和“singleInstance”这两个标记Activity总是新启一个Task的launch mode属性,应该仅仅用于拥有“ACTION_MAIN”和“CATEGORY_LAUNCHER”的Activity。
试想一下,如果没有此Intent filter(main/launcher)什么会发生:一个Intent启动一个“singleTask”的Activity,初始化一个新的Task,而且用户在此Task中做了一定的工作。之后用户按了Home按钮,此Task切换到后台且对用户不可见,此时用户将回不去此Task,因为在Launcher中没有icon呈现。

对于那些你不想用户再回去的Activity,可以设置元素的finishOnTaskLaunch为true。参见Clearing the back stack