1.2.3 Task and Back Stack - 任务和回退堆

         一个应用通常包含多个Activities。每个activity的设计应该围绕着某种指定类型的action,如果这样做了,用户就可以执行该action,也可以用它来开启另外的activity。例如,邮件应用可能会有一个用于展示新邮件列表的页面。当用户选择了一封邮件时,就打开一个新的activity来浏览内容。

         一个activity甚至可以打开存在于同一设备上不同应用里的activity。例如,如果你的应用想发送邮件,你可以定义一个intent来执行这个“发送”的动作,并在intent里带上数据,例如邮件地址和内容等。其它应用中已经申明自己可以处理这种intent类型的activity就会被打开。在这种情况下,该intent用来发送邮件,因此,邮件应用中撰写邮件内容的activity启动(如果多个activity都支持同样的intent,那么系统会让用户选择到底使用哪个activity)。当邮件发送完毕后,你的activity被恢复,这样看起来就好像发送邮件的activity是你应用的一部分。即使activity来自于其它应用,但是android系统通过在同样的任务(task)里保存activity的手段来维持无缝的用户体验。

         当执行某项工作时,任务(task)是与用户相互交互的activity的集合。这些activity被管理在同一个堆中(回退堆(back  stack)),在这个堆里存放的activity是被依次打开过的。

         设备的主屏幕是大多数任务(task)开始的地方。当用户在应用程序启动器上点击一个图标时(或是点击主屏幕上的快捷方式),应用的任务(task)就来到了前台。如果不存在应用的任务(应用最近没有被使用过),那么,一个新的任务就会被创建,并且应用的main  activity就会作为根activity在堆中打开。

         当前的activity启动了另外一个activity时,新的activity就被压入堆顶并获取焦点。前一个activity仍在堆里,但是处于停止状态。当activity停止时,系统会保存它停止时用户界面的状态。如果用户按下了返回键,当前的activity就会从堆中被弹出(activity被销毁了)并且先前的activity被恢复了(先前activity的UI状态被系统记住了)。堆中的activity从不会被重新放置,只会从堆中弹出或是压入--activity启动时被压入堆中,用户使用后退键从activity离开时从堆中弹出。后退堆(back  stack)本身就和“后进先出”的对象结构执行的操作一样。图表1展示了多个activity之间的进程,用时间轴的方式使得这些行为变得可视化,并展示了每个时间点上后退堆的情况。

 

         图1 在后退堆(back  stack)里,如何添加一个新activity任务的展示。当用户按下返回键,当前的activity被销毁,前一个被恢复。

         如果用户继续按下返回键,后退堆(back  stack)里的activity依次被弹出并显示前一个activity,直到用户回到主屏幕(或者是任务(task)开始时运行的activity)。当堆里所有的activity都被移除时,任务(task)也就被销毁了。

         任务(task)是高内聚(原文是cohesive单词,不知道怎么翻译--译者注)的单元,当用户开始一个新的任务(task)或是通过Home键返回到主屏幕时,任务(task)就来到了后台。当任务(task)在后台时,它里面所有的activity都停止了,但是任务(task)的后退堆(back  stack)仍然是完整的--正如图2所示的那样,当其它任务(task)出现时,原先的任务(task)仅仅只是失去了焦点。

 

         图2 两个任务(task):当Task A在后台等待被恢复时,Task B在前台与用户发生交互。

         任务(task)可以返回到前台,因此离开时是什么样子的,回来时仍然是什么样子。例如,假设当前任务(task)(Task A)的堆里有三个activity。用户按下Home键,通过应用程序加载器开启了一个新的应用。当主屏幕显示时,Task A进入了后台。当新的应用开启时,系统为新应用创建了一个属于它的新的任务(task)(Task B)。在和新的应用发生完交互后,用户再次回到了主屏幕并打开了先前的应用。这时,Task A回到了前台--它堆里的三个activity都是完整的,并且堆顶部的activity被恢复。此时,用户也可以通过回到主屏幕、点击应用图标来启动Task B(或者从最近应用的屏幕上选择Task B的任务(task))。上面说的就是Android系统里多任务处理的例子。

         注意:虽然在后台一次可以拥有多个任务,但是,如果在同一时刻用户在后台运行了很多任务,那么系统为了恢复内存,可能会干掉某些后台activity,这样就会引起activity状态的丢失。更多详情请参见Activity state 段落。

         因为后退堆(back stack)里的activity从不重新放置,那么,如果从多个activity里开启了同一个activity,那么,该acitivty的新实例就会被创建被压入堆中(而不是把该activity的原先实例带到堆顶)。应用中的activity本身就可以被实例化多次(即使是来自不同的任务(task)),如图3所示的那样。

 

         图3 同一个activity被实例化多次

         如果用户使用返回键返回时,activity的每个实例以它们打开的顺序依次被展示(每个activity都保存着它原先的UI状态)。虽然如此,但是,如果你不想一个activity被实例化多次,你可以改变这种状态。在随后的 Managing Tasks 段落里讨论了如何实现它。

         activity的默认行为和任务(task)的总结:

         1.  当activity A启动了Activity B时,activity A停止了,但是系统会保持它的状态(例如滚动的位置或是表单里输入的文本等)。如果用户使用返回键从activity B返回到activity A时,activity A会带着被存储的状态一起被恢复。

         2.  当用户通过按下Home键返回到主屏幕的方式离开一个任务(task)时,当前的activity停止了,并且它的任务(task)进入了后台。系统会保持任务(task)里每个activity的状态。随后,如果用户通过点击应用图标再次打开应用时,该应用的任务(task)就回到了前台并恢复了任务(task)堆顶部的activity。

         3.  如果用户点击了返回键,当前的activity从堆中弹出并销毁了。堆中先前的activity被恢复了。系统不会保持被销毁activity的状态。

         4.  activity可能会被实例化多次,即使是来自于不同的任务(task)。

Saving Activity State - 保存Activity状态

         正如上边讨论的那样,系统默认会保存已经停止的activity的状态。所以当用户回到先前的activity时,它的用户界面仍然和用户离开时的一样。虽然如此,但是,为了防止activity被销毁和重建,你也应该主动地使用回调方法保存activity的状态。

         当系统停止你的activity时(例如新的activity启动了,或者是任务(task)进入后台了),如果需要恢复内存,系统可能会完全销毁掉某个activity。如果这样的事情发生了,那么与销毁activity相关的信息也都丢失了。虽然被销毁activity的实例仍然保存在后退堆(back stack)中,但是当它到达堆顶时,系统必须得将它重建(而不是恢复)。为了避免丢失掉用户的操作,你应该在activity里通过实现onSaveInstanceState() 回调方法来主动地保存activity的状态。

         关于如何保存activity的更多详情,请参见onSaveInstanceState() 章节。

Managing Tasks - 管理任务(task)

         Android中,管理任务(task)和后退堆(back stack)的办法是把所有的activity连续地放置在同一个任务(task),同一个“后进先出”的堆里--对大多数应用来讲,这样做是非常棒的。你不用考虑你的activity如何与任务(task)相联系,也不用考虑它们在后退堆里如何存在。虽然如此,但是,你或许想要中断正常的行为。或者当你应用中启动某个activity时,你想把它放入一个新的任务(task)里(而不是放在当前堆里),或者,当你打开一个activity时,你想把它的一个已经存在的实例带到前台(而不是在后退堆(back stack)里重新实例化一个),再或者,当用户离开任务(task)时,你想把你后退堆(back stack)里除了根activity的所有其它的activity都从后退堆(back stack)里移除掉。

         上面所说的你都可以做到,甚至做到更多,方法是使用mainfest文件里<activity> 元素的属性,或者在使用startActivity() 方法打开activity时,给该方法传递带有标记的intent即可。

         你可以使用的<activity> 主要的属性如下:

        taskAffinity launchMode

        allowTaskReparenting

        clearTaskOnLaunch

        alwaysRetainTaskState

        finishOnTaskLaunch

         你可以使用的主要intent的标记如下:

        FLAG_ACTIVITY_NEW_TASK

        FLAG_ACTIVITY_CLEAR_TOP

        FLAG_ACTIVITY_SINGLE_TOP

         在下面的内容里,你将会学习到如何使用这些属性和intent标识来定义activity与任务(task)怎样进行联系,activity在后退堆(back stack)里又有怎样的表现。

         警告:大多数应用不应该中断activity和任务(task)的默认行为。如果你决定了你的应用必须改变activity的默认行为,那你一定要小心的使用,并一定要测试当从其它activity返回时、当使用返回键从其它任务(task)返回时它的可用性。一定要测试是否有与用户所期望的行为冲突的导航行为。

Defining launch modes - 定义启动模式

          启动模式可以你让来定义一个activity的新实例如何与当前任务(task)进行联系。你可以使用两种方法来定义不同的启动模式:

        Using  the mainfest file

        你在清单文件里申明一个activity时,你就可以定义在它在启动时如何与任务(task)进行联系。

Using Intent flags

         当你调用startActivity()方法时,你可以在Intent 参数里包含一个标识来申明当前的activity如何/是否与当前任务(tack)发生联系。

         从根本上说 ,如果Activity A启动了Activity B,在清单文件里可以定义Activity B如何与当前任务(task)联系,并且Activity A也可以要求Activity B怎样与当前任务(task)联系,如果两个Activity都定义了Activity B如何与当前任务(task)发生联系,那么Activity A的请求(定义在intent里的)的优先级高于Activity B的要求(定义在清单文件里的)。

         注意:在清单文件里可用的启动方式在intent里不一定能找到相应的标识,同样地,在intent标识里可用的启动方式在清单文件里也不一定能找到相应的申明。

         Using  the mainfest file

         你在mainfest里申明一个activity时,你可以使用<activity>元素里的launchMode 属性。

         launchMode 属性指定了关于activity如何被加载到任务(task)的说明。有四个可以指定给launchMode 属性的不同的读取模式:

         "standard"(默认模式)

                 默认地,系统在activity开始的任务(task)里给它创建一个新的实例并把intent发送给它。该activity可以被实例化多次,每个实例都可以属于不同的任务(task),并且一个任务(task)里也可以有它的多个实例。

         "singleTop"

                在当前任务(task)的顶部,如果一个activity的实例已经存在了,那么系统会通过调用该实例的 onNewIntent() 方法来把intent发送给它,而不是再创建一个新的实例。activity可以被实例化多次,每个实例都可以属于不同的任务(task),并且一个任务(task)里也可以有它的多个实例(当activity处于后退堆(back stack)的顶部时,不是它的一个现有实例)。

                例如,假设一个任务(task)的后退堆(back stack)由根activity A和activity B、C、D组成(D在顶部)。类型D的intent到达了。如果D是默认的启动模式"standard",那么D的一个新实例被启动了,堆里的内容就变为A-B-C-D-D。虽然如此,但是,如果D的启动模式是"singleTop",那么,已存在的D的实例就会通过onNewIntent() 方法收到该intent,这是因为它在堆顶--堆里的内容仍然为A-B-C-D。但是,如果类型B的intent到达,那么即使B的启动模式是"singleTop",系统也仍然会新实例化一个B的实例并把它添加到 堆里。

                注意:activity的新实例被创建时,用户可以通过按下返回键返回到先前的activity。但是,当一个activity已经存在的实例处理了一个新的intent,那么,在一个新的intent进入onNewIntent() 方法之前,用户不能通过按下返回键返回activity的状态。

          "singleTask"

                系统会创建一个新的任务(task)并把activity的实例做为该任务(task)的根。但是,如果该activity的实例在一个单独的任务(task)里已经存在了,那么系统会通过调用它的onNewIntent() 方法来把intent发送给它,而不是再创建一个新的实例。也就是说,在同一时刻,仅仅只会存在该activity的一个实例。

                注意:尽管在新的任务(task)里启动了该activity,但是按下返回按钮时,用户仍然会回到先前的activity。

         "singleInstance"

                与"singleTask"类似,不一样的是系统不会把其它的activity发送到拥有"singleInstance"启动模式activity的任务(task)里。以该模式启动的activity在任务(task)只会有一个;任何以该模式启动的activity都会在一个单独的任务(task)里被打开。

         其它的例子是Android浏览器应用,它通过在清单文件的<activity>元素里指定singleTask模式来申明浏览器里的activity都应该在它自己单独的任务(task)里打开。这就意味着,如果你的应用发布了一个intent来打开网页浏览器,那么它的activity不会和你的应用放置在同一个任务(task)里。要么是一个新的任务(task),要么如果浏览器已经在后台运行了,那它的任务(task)就会被带到前台来处理该intent。

         当一个activity启动另外一个时,不管另外一个activity是在新任务(task)里启动还是在原有任务(task)里启动,按下返回键总会使用用户回到先前的activity。但是,如果你使用singleTask模式启动了一个activity,那么,如果它的一个实例已经存在于后台的某个任务(task)中,那这个任务(task)会整个都被带到前台的。如果是这种情况,后退堆(back stack)里包含了任务(task)里的所有activity。图4就描述了这种情况:

 

         图4  描述了以"singleTask"模式启动的activity是如何被添加到后退堆(back stack)里的。如果该activity已经存在于某后台任务(task)(有自己的后退堆(back stack)),那么,当该activity启动到前台时,它所在的整个后退堆都被带到前台了,并放置于当前任务(task)的顶部。

         关于在清单文件里使用启动模式的更多详情,请参见<activity>元素文档,在该文档里,launchMode属性与接收的值有更多的讨论。

         注意:在清单文件使用 launchMode 属性为activity指定的启动模式会被包含在启动activity的internt的标识重写,下面的段落会讨论这一情况的。

         Using Intent flags

         当你使用startActivity()方法打开一个activity时,你可以通过给该方法传递包含标识的intent来改变它与任务(task)默认的联系方式。你可以用来改变默认行为的标签是:

         FLAG_ACTIVITY_NEW_TASK

         在新的任务(task)里开启activity。如果要启动的activity已经存在于某个任务(task)里,那么该任务(task)会以它最后存储的状态出现在前台,并且,该activity会在onNewIntent() 方法里接收到新的intent。

         它会和清单文件里申明"singleTask"产生同样的效果。

         FLAG_ACTIVITY_SINGLE_TOP

         如果在当前的任务(task)里启动(在后退堆(back stack)的顶部),那么,已经存在的实例会接收到onNewIntent() 方法的调用,而不是创建一个新的实例。

         它会和清单文件里申明"singleTop"产生同样的效果。

         FLAG_ACTIVITY_CLEAR_TOP

         如果要启动的activity已经存在于当前任务(task)了,那么不是启动一个activity的新实例,而是把任务(task)里在该activity之上的所有其它activity的实例都销毁掉,然后通过onNewIntent() 方法把intent分发给该activity原先的实例(该activity当前在任务(task)的顶部,被恢复了)。

         清单文件里没有对应的值。

         FLAG_ACTIVITY_CLEAR_TOP经常联合FLAG_ACTIVITY_NEW_TASK一起使用。如果它们俩个一起使用了,那么这是一种可以定位在其它任务(task)里已经存在activity并把它放置到可以响应intent位置的方法。

         注意:如果指定activity的加载模式是"standard",那么它也会从堆中移除,并创建一个新的实例来处理到达的intent。这就是为什么当启动模式是"standard"时总是会为新的intent创建一个新的实例。

Handling affinities - 处理亲疏关系

         亲疏关系(affinity)标识着activity更愿意属于哪个任务(task)。默认地,来自于同一应用的所有activity都有着相同的亲疏度(affinity)。因此,同一应用的所有activity都更愿意进入同一任务(task)。但是,你也可以更改某个activity的默认行为。定义在不同应用里的activity也可以拥有同样的亲疏度,或者同一应用里的不同activity有着不一样的亲疏度。

         你可以使用<activity>元素的taskAffinity  属性来改变activity的亲疏度。

         taskAffinity  属性的取值是字符串类型,该值必须是唯一的,它来自于在<manifest> 元素里申明的默认包名(此句翻译感觉有问题,原句是:The taskAffinity attribute takes a string value, which must be unique from the default package name declared in the <manifest> element  -- 译者注),因为系统使用该名字来为应用定义默认任务(task)的亲疏度。

         亲疏度在两种情况下起作用:

          1.  当包含了FLAG_ACTIVITY_NEW_TASK 标识的intent启动某个activity时。

         调用startActivity()方法启动的新activity默认被装载进入任务(task)中。它和调用者一样被压入同样的后退堆(back stack)中。但是,如果传递 给startActivity()方法的intent包含了FLAG_ACTIVITY_NEW_TASK 标识,那么系统就会查找一个不同的任务(task)来给新的activity安家。通常,这是一个新的任务(task),但不是必须得是新的。如果已经存在的任务(task)和新的activity有着同样的亲疏度,那么新activity就被加载到这个任务(task)里了。如果没有与新activity有着同样亲疏度的任务(task),那系统就会新开一个任务(task)。

         如果FLAG_ACTIVITY_NEW_TASK 标识使得activity新开了一个任务(task),然后用户按下返回键离开了该activity,那么肯定会有某种方式让用户可以返回到这个任务(task)。某些实体(例如通知管理器)总是在一个外部的任务(task)里打开activity,从来不会在自己所在的任务(task)里打开,因此,它们在使用startActivity()方法时总是给intent绑定FLAG_ACTIVITY_NEW_TASK 标识。如果你的activity可能被外部的实体执行,那你可能会用到这个标签,要千万小心,用户可能会使用其它方法返回到打开activity的任务(task)里,例如使用快捷方式图标(任务(task)里的根activity有一个CATEGORY_LAUNCHER 的intent过滤器,下面Starting a task会讲述这些内容)。

         2.  当activity的 allowTaskReparenting 属性被设置为true时。

         在这种情况下,当任务(task)来到前台时,activity会从启动它的任务(task)里移动到与它有着同样亲疏度的任务(task)中。

         例如,假设一个展示被选择城市天气情况的activity被定义在一个旅游应用里。它和应用里其它的activity有着同样的亲疏度(默认的应用亲疏度),并且它设置了允许重复的属性。当你应用的某个activity启动了展示天气情况的activity时,它最初和启动它的activity在同一个任务(task)里。但是,当旅游应用所在的任务(task)回到前台时,展示天气情况的activity会再次指定给这个任务(task)并在它里面进行显示。

         提示:如果以用户的角度来看,一个.apk文件里包含了多于一个的应用时,你就可以给与不同应用联系的不同activity使用taskAffinity属性来分配不同的亲疏度。

Clearing this back stack - 清理后退堆

         如果用户离开任务(task)很长时间了,系统会清理任务(task)里除了根activity的其它所有activity。当用户再次返回到任务时,仅仅只有根activity被保存下来了。因为在用户长时间的离开后,他很可能不再关心他之前的改变了,重新回到任务里,他很可能想做一些新的事情,系统就是基于这种考虑才会清理 任务的。

         你可以使用下面的activity的属性来对这一行为做出改变:

        alwaysRetainTaskState

         如果任务(task)的根activity的该属性被设置为true,那么上面描述的默认行为就不会发生了。即使经过了很长一段时间后,任务(task)仍然会保存堆里所有activity的状态。

        clearTaskOnLaunch

         如果任务(task)的根activity的该属性被设置为true,不论用户什么时候离开任务(task),什么时候又回来,堆里面只会留下根activity,其它的都被清理了。

        finishOnTaskLaunch

         这个属性和clearTaskOnLaunch 属性类似,但是它只会操作一个单独的activity,而不是整个任务(task)。它可以让任何一个activity消失,包括根activity。如果它被设置为true,系统会为当前session保存部分activity。如果用户离开后返回到任务(task)里,它就不再存在了。

Starting a task - 开始一个任务

         你如果给一个activity设置intent过滤器的指定action为"android.intent.action.MAIN",指定的category为"android.intent.category.LAUNCHER",那么,这个activity就成为一个任务(task)的入口了。

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

 

         这种intent过滤器会让该activity的图标和文本显示在应用程序加载器的面板上,这样的话就给用户提供了一种方式来加载activity,并在加载之后的任何时间都可以返回到它创建的任务(task)里。

         第二点非常重要:在离开任务(task)以后,用户肯定可以通过activity加载器返回到原先的任务(task)里。因为这个原因,当activity有ACTION_MAIN和CATEGORY_LAUNCHER过滤器时,应该使用"singleTask"和"singleInstance"这两种标识着activity总是实例化同一个任务(task)的启动模式中的某一个。想象一下,如果过滤器丢失了会发生什么:一个intent装载了拥有"singleTask"模式的activity,这样它就会实例化一个新的任务(task),用户还花费了一些时间在这个任务(task)上做了一些工作。然后,用户按下了Home键。这时,任务(task)进入了后台并不可见了。现在,用户再也不能返回到这个任务了,这是因为没有ACTION_MAIN和CATEGORY_LAUNCHER过滤器的应用不会在应用程序加载器上出现的。

         你如果不想让用户返回到某个activity,那你就把它<activity>元素的finishOnTaskLaunch 属性设置为true就可以了。

posted @ 2013-09-06 22:27  wchhuangya  阅读(1912)  评论(2编辑  收藏  举报