API指南—Activity的任务和后退栈
一个应用程序通常会包含很多个Activity。每个activity都应该满足用户的某种操作需求,也可以打开别的activity。例如,一个电子邮件应用程序可能有一个activity用来显示新邮件列表。当用户选择某个新邮件的时候,会打开一个新的activity用来查阅这封邮件。
一个activity也可以启动别的应用程序中的某个activity。例如,如果你的应用程序想发一封邮件,你可以定义一个intent,去执行一个”发送”动作,并且包含一些数据,比如邮件的地址和内容。别的应用中的某个activity,如果它声明了它可以处理你的intent的话,它就会启动。在这种情况下,这个intent企图发送一封邮件,那么邮件应用的某个activity就会启动(如果有多个activity可以处理这个intent的话,那么系统会让用户决定选择哪个)。当邮件发送完以后,你的activity就会继续运行,这样看起来就好像那个负责发送邮件的activity属于你的应用一样。尽管这些activity来自不同的应用,但是安卓通过将这些activity保存在同一个任务中来保持无缝的用户体验。
任务就是一组用户能与之交互并完成特定事情的activity。这些activity排列在一个栈中(名叫后退栈),按照每个activity打开的顺序排列。(译者注:任务是一个抽象概念,而栈是一个物理概念,可以说栈是任务的物理表示。)
设备的主屏幕是启动大多数任务的地方。当用户点击屏幕上的某个应用图标(或快捷图标),这个应用的任务就会进入前台。如果这个应用还没有任务存在(意思是此应用并未在后台工作),那么系统会创建一个新任务,并且启动应用的主activity,并把这个activity作为栈的根activity放进栈中。(译者注:根activity其实就是栈底activity。)
如果当前的activity启动了另一个activity,那么这个新的activity就会被压进栈顶,而后获得焦点。前一个activity仍保留在栈中,只是进入了停止状态。当一个activity停止的时候,系统会保留它的UI的状态。当用户点击后退键,当前activity从栈顶弹出(此activity被销毁),然后前一个activity进入运行状态(它的UI状态会被恢复)。栈中的activity永远不会重新排列,只能压栈或弹栈---如果被当前activity启动就压栈,如果用户点击后退键离开它就被弹栈。因此,后退栈的操作是基于“后进先出”的对象结构。图1使用时间线刻画了每个时间点上这些activity在栈上的操作变化。
图1 说明了每个新的activity是如何进入后退栈的。当用户点击后退键时,当前activity就会被弹栈销毁,前一个activity接着运行。
如果用户不停点击后退键,那么栈中的每个activity就会被依次弹栈,同时显示前一个activity,直到用户返回到主屏幕(或者回到此任务开始前的某个activity)。当任务中所有的activity都从栈中被移除的话,这个任务也就不再存在了。
当用户开始一个新的任务或者点击Home键进入主屏幕,当前任务都会作为一个整体进入后台。在后台的时候,任务中所有的activity都进入停止状态,但是任务栈仍然是完整的—--此任务只是失去焦点,而被另一个任务夺得焦点而已,如图2所示。
图2.两个任务:任务B进入前台获得用户焦点,而任务A进入后台,等待继续运行。
当任务进入后台时,用户仍然可以选择它,而使它再次进入前台。例如假设当前任务(任务A)栈中有3个activity---2个activity在当前activity下面。用户点击Home键,然后启动某个新的应用。当主屏幕显示的时候,任务A就进入了后台。当新的应用启动的时候,系统就会为这个应用启动一个新的任务(任务B),创建它的任务栈。当用户与此新应用交互完成后,返回到主屏幕,然后选择之前启动任务A的应用。那么现在,任务A就进入了前台----栈中的3个activity仍然是完整的,并且栈顶的那个就会进入运行状态。此时,用户仍然可以先进入主屏幕再选择启动任务B的应用来切换到任务B(或者通过“最近使用的应用程序”窗口来选择任务B)。这就是安卓上多任务的一个例子。
注意:多个任务可以同时保存在后台。但是如果用户在后台保存了太多的任务,当系统回收内存的时候,它会销毁某些后台activity,这样就会引起这些activity的状态消失。请查看后面关于activity状态的描述。
由于后退栈中的activity永远不会被重新排列,如果你的应用允许用户通过多个activity来启动某一个activity的话,那么这个activity的新实例就会被创建然后压栈(而不是把之前存在的实例放入栈顶)。正因如此,你的应用中的某个activity可能被创建多个实例(被来自不同的任务创建),如图3.
图3. 一个activity被创建多个实例。
正如此,如果使用后退键返回,那么这个activity的每一个实例都会按照打开的顺序依次显示(每一个都拥有自己独立的UI状态)。尽管如此,如果你不想某个activity被实例化超过一次的话,你可以修改这种行为。如果做到这一点,我们在后面的“管理任务”中讨论。
总结一下activity和任务的默认行为:
当Activity A启动Activity B,Activity A停止,但是系统保留它的状态(比如滚动位置或者是表单中的文本)。如果用户在Activity B中点击后退键,Activity A从旧状态中恢复并继续运行。
当用户点击Home键离开某个任务,当前的activity停止,而且它所在任务进入后台。系统保留任务中的每一个activity的状态。如果用户选择启动此任务的应用图标,那么这个任务就会进入前台,并且栈顶的activity继续运行。
如果用户点击后退键,当前activity从栈顶弹出并销毁。栈中的前一个activity继续运行。当一个activity销毁的时候,系统不再保留它的状态。
一个activity可以被实例化多次,即使从其他任务中打开。
保存activity状态
如上讨论,当activity停止的时候,系统默认会保存它的状态。这样的话,当用户再次回到前一个activity的时候,它的用户界面就会和原来保持一样。但是,你能—--也应该----使用回调方法主动保存你的activity的状态,以防止万一这个activity被销毁而被重建。
当系统停止你的某个activity(比如启动一个新的activity或者当前任务进入后台),系统可能在需要回收内存的时候完全销毁你的那个activity。当发生这种情况的时候,关于那个activity的信息都会丢失。如果发生了这种情况,系统仍然知道那个activity在栈中的位置,当这个activity成为栈顶元素的时候,系统只能重建它(而不是继续运行它)。为了避免用户工作的丢失,你应该主动地在activity的回调方法onSaveInstanceState()中保存这些工作。
管理任务
如果所述,安卓管理任务和后退栈的方式----把依次启动的activity放入同一个任务中并保存在一个“后进先出”中的栈中----针对大多数应用来说工作得很好,你应该无须担心你的activity是如何与任务关联的以及是如何存在于后退栈中的。但是,你可能还是想干涉这种正常行为。也许你想你的某个activity被启动的时候,开始一个新任务(而不是保存在当前任务中);或者,当你启动一个新的activity的时候,你想把一个已经存储的实例放入栈顶(而不是在栈顶重新创建一个实例);或者,当用户离开某个任务的时候,你希望除了根activity之外,栈中所有的activity都要被清除。
你可以做到上述这些事情,而且还得做得更多,一是通过<activity>清单属性,一是通过传递给startActivity()的相应地intent标志。
关于此,你可以使用的<activity>的主要属性有:
· taskAffinity
· launchMode
· allowTaskReparenting
· clearTaskOnLaunch
· alwaysRetainTaskState
· finishOnTaskLaunch
你可以使用的主要的intent标志有:
· FLAG_ACTIVITY_NEW_TASK
· FLAG_ACTIVITY_CLEAR_TOP
· FLAG_ACTIVITY_SINGLE_TOP
在接下来的部分,你将会看到你应该如何使用这些清单属性和intent标志来定义activity如何与任务关联以及它们在后退栈中的行为。
警告:大多数应用都不应该干涉activity和任务的默认行为。如果你决定有必要更改这种默认行为,请小心使用并且测试一下activity启动时,和使用后退键从其它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的标志,同样如此,有一些启动模式只能作为Intent的标志,而无法在清单文件中定义。
使用清单文件
当在清单文件中声明一个activity时,你可以使用<activity>元素的launchMode属性来指定它如何与某个任务关联。
launchMode属性指示一个activity如何加载进某个任务中。你可以为属性launchMode指定4种不同的启动模式:
“standard”(默认模式)
默认值。系统会在启动它的任务中创建此activity的新实例,并且会把intent传递给它。这个activity可以被实例化多次,每一个实例都可以属于不同的任务,并且一个任务也可以包含多个实例。
“singleTop”
如果某个activity实例已经存在于当前任务的栈顶,系统会通过它的onNewIntent()方法传递给该实例intent对象,而不会再创建一个此activity的新实例。这个activity同样可以被实例化多次,每个实例也可能属于不同的任务,一个任务也可以包含多个实例(但是只要当前栈顶的activity不是此activity的某个已存在的实例就行)。
例如,比如当前任务的后退栈包含一个根Activity A和Activity B, C和D,D为栈顶(栈中元素为A-B-C-D;D为栈顶)。如果一个Intent要求启动D,假设D使用默认启动模式”standard”,那么D的新实例就会被创建,栈的顺序就会变为A-B-C-D-D。但是如果D使用”singleTop”启动模式,那么栈顶的D就会通过onNewIntent()方法收到Intent,栈的顺序依然为A-B-C-D。但是如果一个Intent要求启动B,即使B的启动模式是”singleTop”,一个新的B实例依然会被创建,栈就会变成A-B-C-D-B。
注意:如果某个activity的新实例被创建,用户可以点击后退键返回到之前的activity状态。但是如果是一个已存在的activity通过onNewIntent()来处理一个新的Intent,那么用户点击后退键,是无法返回到此activity之前的状态的。
“singleTask”
系统会创建一个新任务,并且把此activity作为新任务的根activity。但是,如果此activity的某个实例已经存在于另一个任务中,那么系统会通过它的onNewIntent()方法将intent传递给这个实例,而不会再创建一个新的实例。在同一时间内只能有一个实例。
注意:尽管这个activity在一个新任务中启动,点后退键仍然会让用户回到该新任务栈中的前一个activity。
“singleInstance”
与”singleTask”相同,但是系统不会在它所在的任务栈中放入其它的activity。这个activity总是它所属任务的唯一成员;任何被它启动的activity只能在其他任务中打开。
举个例子来说,安卓浏览器应用总是指定它的activity在自己的任务中打开---通过指定<activity>元素的启动模式为singleTask。这意味着如果你的应用发送一个Intent来启动浏览器,它的activity是不会放在你的任务中的。而是要么为浏览器启动一个新任务,要么如果浏览器已经运行在某个后台任务中的话,那个任务就会进入前台来响应新的Intent。
无论某个activity是在一个新任务中启动或者与启动它的activity在同一任务中,后退键总是会让用户回到前一个activity。虽然如此,如果你启动了一个使用singleTask启动模式的activity,那么如果这个activity的某个实例已经存在于一个后台任务中,那整个后台任务就会进入前台。在此时,任务栈中的所有activity都一起进入了前台,且位于栈顶(译者加:我认为“且位于栈顶是错误的”,它的意思是说把别的任务栈中的所有activity取出来放入了当前的任务栈,显示这是错误的,因为两个任务栈是独立的。)如图4描述了这种情况。
图4.
注意:你使用launchMode属性指定的activity启动模式是可以被启动你的activity的Intent标志给覆盖的,在下个部分会讨论这个。
使用Intent标志
当启动activity的时候,你可以在传递给startActivity()的Intent中包含某个标志来更改此activity与任务的默认关联。你可以使用的用来更改默认行为的标志有:
FLAG_ACTIVITY_NEW_TASK
在新任务中启动此activity。如果已经有一个为此activity运行的任务,那么这个任务就会被带回前台,并且恢复至最近状态,同时这个activity会在onNewIntent()中收到新的Intent。
这个标志和launchMode=”singleTask”产生的行为一样,我们在之前的部分已经讨论过。
FLAG_ACTIVITY_SINGLE_TOP
如果要启动的activity就是当前的activity(即位于后退栈顶),那么这个已存在的实例就会收到onNewIntent()回调,而不会再创建一个新的实例。
这个标志的行为与launchMode=”singleTop”一样,如前所述。
FLAG_ACTIVITY_CLEAR_TOP
如果要启动的activity已经在当前任务中运行,那么无须创建此activity的新实例,而是将栈中处于此实例之上的activity全部销毁,并且把intent通过此实例的onNewIntent()传递给它。
在launchMode属性值中无对应关系存在。
FLAG_ACTIVITY_CLEAR_TOP经常与FLAG_ACTIVITY_NEW_TASK一起使用。当一起使用的时候,这两个标志意味着在另一个任务中定位某个已存在的activity,然后将其置于适当的位置处理新的Intent。
注意:如果指定的activity的启动模式是”standard”,那么这个activity也会被从栈中移除,并且会创建它的一个新实例来响应Intent。主要是因为当启动模式是”standard”时,总是会创建一个新实例。
处理亲向值
亲向值表明了某个activity更愿意属于哪一个任务。默认情况下,在同一个应用中的所有activity彼此关系是较亲近的。所以,默认情况下,同一应用中的所有activity更喜欢处于同一任务中。然而,你可以更改某个activity的亲向值。不同应用中的activity可以共享同一个亲向值,或者同一应用中的所有activity可以被赋予不同的任务亲向值。
你可以使用<activity>元素的taskAffinity属性来更改某个activity的任务亲向值。
taskAffinity属性接受一个字符串值,此值必须与<manifest>元素中声明的默认包名不同,因为系统会使用这个名字来标志某个应用的默认任务亲向值。
这个亲向值在以下两种情况下起作用:
1) 当启动某个activity的Intent中包含标志FLAG_ACTIVITY_NEW_TASK
默认情况下,一个新的activity会被放置在调用startActivity()的activity所在的任务中。它会被放置在与调用者一样的后退栈中。然而,如果传递给startActivity()的Intent中包含FLAG_ACTIVITY_NEW_TASK标志,那么系统会寻找一个不同的任务来放置这个新的activity。通常情况下,是一个新任务。如果已经存在了某个任务的亲向值与此activity的一样,那么,这个activity就会被放入到这个任务中。否则的话,会创建一个新任务。
如果这个标志引起某个activity被放置在了一个新任务中,那么当用户点击Home键离开的话,应当保证有方法让用户重新返回到这个任务中。一些实体类(比如通知管理器NotificationManager)经常在另外一个任务中启动activity,从未让activity属于他们自己的任务,所以他们经常在传递给startActivity()中的Intent中添加FLAG_ACTIVITY_NEW_TASK标志。如果你拥有一个可以被某个外部实体类使用此标志调用的activity,那么请注意应该让用户有独立的方法再次返回到这个任务中,比如使用启动图标(任务的根activity有一个CATEGORY_LAUNCHER Intent过滤值)。
2) 当某个activity的allowTaskReparenting属性值为”true”
在这种情况下,当activity的亲向值指定的任务进入前台时,这个activity可以从它目前所在的任务中移动到亲向值指定的任务中去。
例如,在一个旅游类应用中有一个activity用来报告所选城市的天气情况。它与应用中其它的activity有相同任务亲向值(默认的应用亲向值),同时它也设置了allowTaskReparenting=true。当你的某个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设置一个intent过滤器,其中指定action为” android.intent.action.MAIN”, category为“android.intent.category.LAUNCHER”,就能将此activity指定为任务入口点。例如:
<activity ... >
<intent-filter ... >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...
</activity>
此类intent过滤器设置将会使某个activity的图标和标签显示在应用启动器中,用户可以通过此方法启动此activity,而且在以后的任何时候都能够返回到此任务中。
第2个能力很重要:用户必须能够在离开某个任务后,然后可以通过activity启动器图标再次返回到任务中去。正是基于这个原因,”singleTask”和”singleInstance”这两个总是初始化一个任务的启动模式,应当在activity拥有”ACTION_MAIN”和”CATEGORY_LAUNCHER”过滤器的情况下使用。例如,设想一下没有这个过滤器的话会发生什么:一个Intent启动了一个”singleTask” activity,初始化一个新任务,并且用户在这个任务中做了一些事情。然后用户点击了Home键。这个任务现在进入了后台且不可见了。现在用户没有办法再次返回到该任务中去了,因为这个任务在应用启动器中没有入口点。
在所有的这些情况中,如果你不想让用户再回到某个activity中,你可以设置<activity>元素中的finishOnTaskLaunch属性值为true。(具体参考上面清除栈)