1.2 Activity和任务
如前所述,一个Activity可以启动其他的,包括不同应用程序定义的Activity。例如,假设想让用户看到某个地方的街道地图。而且已经有一个Activity具有此功能,那么我们的Activity只需要把必要的信息打包到Intent对象并把它传递给startActivity()。于是地图浏览器就会显示那个地图。而当用户按下BACK键的时候,我们的Activity又会再次显示在屏幕。
对于用户而言,这看起来就像是地图浏览器是我们的Activity所在的应用程序中的组成部分,尽管它是由其他应用程序定义,并运行在那个应用程序的进程之中。Android将这两个Activity放在同一个任务中来维持一个完整的用户体验。简单的说,任务就是用户所体验到的“应用程序”。它是安排在一个堆栈中的一组相关的Activity。堆栈中的根Activity就是启动任务的Activity——通常,它就是用户在应用程序加载器中所选择Activity。而堆栈最上方的Activity则是当前运行的——直接响应用户操作。当一个Activity启动另外一个的时候,新的Activity就被压入堆栈,并成为当前运行的Activity。而前一个Activity仍保持在堆栈之中。当用户按下BACK键的时候,当前Activity出栈,而前一个恢复为当前运行的Activity。
堆栈中保存的其实是对象,所以如果堆栈有相同Activity之类的多个实例(例如多个地图浏览器),堆栈会独立保存每个实例。堆栈中的Activity永远不会重新排列,只有压栈和出栈。
任务其实就是Activity的堆栈,而不是manifest文件中的一个类或者元素。所以我们无法撇开Activity而为一个任务设置一个值。而事实上整个任务使用的值是在根Activity中设置的。例如,下一节我们会谈及“任务的Affinity”,这个值就是根Activity的Affinity设置中读出。
任务中的所有Activity是作为一个整体进行移动的。整个任务(整个Activity堆栈)可以移到前台,或退至后台。例如,设想当前任务堆栈中存有四个Activity——三个在当前Activity之后。当用户按下HOME键回到应用程序加载器,然后选择一个新的应用程序(事实上是一个新任务)。则当前任务进入后台,而新任务的根Activity显示出来。然后,用户很快再次回到应用程序加载器而又选择了前一个应用程序(前一个任务)。于是前面的任务,包括堆栈中所有的四个Activity,再次回到前台。当用户按下BACK键的时候,屏幕不会显示出用户刚才离开的Activity(上一个任务的根Activity)。取而代之,当前任务堆栈中最上面的Activity被弹出,而同一任务中的上一个Activity被显示出来。
上述行为都是Activity和任务的默认行为。但是有方法可以改变所有这一切。Activity和任务的联系、任务中Activity 的行为方式,都是由启动Activity的Intent对象中设置的一系列标记和manifest文件中对应Activity中的<activity>元素的系列属性相互协作进行控制。请求发出者和回应者在这里都拥有话语权。
我们刚才所说的关键Intent标记如下:
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
FLAG_ACTIVITY_SINGLE_TOP
而主要的<activity>属性是:
taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch
接下来的一节会描述这些标记以及属性的作用,它们是如何互相影响的,以及使用它们的时候必须考虑的因素。
1.2.1 Affinity(亲缘性)和新任务
默认时,应用程序中的所有Activity相互之间都有一种Affinity——即,它们都属于相同的任务。但是,可以在<activity>元素的taskAffinity属性为每个Activity设置独立的Affinity。因此不同应用程序定义的Activity可以共享系统的Affinity、定义在相同应用程序中Activity也可以有不同的Affinity。Affinity在两种情况下产生作用:当启动Activity的Intent对象包含了FLAG_ACTIVITY_NEW_TASK 标记,或者当Activity的allowTaskReparenting属性设置为“true”时。
v FLAG_ACTIVITY_NEW_TASK标记
如前所述,默认时,新的Activity会加入到调用startActivity()方法的Activity所在的任务之中。并压入了调用者所在的相同堆栈。但是,如果传递给startActivity()的Intent对象包含了FLAG_ACTIVITY_NEW_TASK标记,系统会为新Activity寻找一个不同的任务。通常,正如标记所暗示的那样,是一个新任务。但这并不是必然的。如果已经存在一个与新Activity有相同Affinity的任务,则新Activity会加入那个任务之中。如果不存在,则开启新的任务。
v allowTaskReparenting 属性
如果Activity的allowTaskReparenting属性设置为“true”,当具有与相同Affinity的任务转到前台时,该Activity就可以从初始的任务转移到该任务之中。例如,设想一个旅行应用程序定义了一个预报所选城市天气的Activity。它与这个应用程序中的其它Activity具有相同的Affinity(默认的Affinity)而且允许重定父级。我们的一个Activity启动了天气预报,因此它与我们的Activity属性相同的任务。但是,当旅行应用程序再次回到前台的时候,这个天气预报Activity会被再次调整到原先的任务之中并显示。
如果一个.apk文件中包含了多个用户视觉看到的“应用程序”,我们可能需要为相关的Activity赋予不同的Affinity。
1.2.2 启动模式
元素<activity>的launchMode属性可以设置四种不同的加载模式:
“standard”(默认值)
“singleTop”
“singleTask”
“singleInstance”
这些模式之间的差异主要体现在四个方面:
v 哪个任务会保留对Intent做出响应的Activity。对于“standard”和“singleTop”模式,是生成Intent(并调用 startActivity())的任务——除非Intent对象包含FLAG_ACTIVITY_NEW_TASK标记。如果设定了该值,正如前一节Affinitie和新任务所述,会选择不同的任务。
相反,对于“singleTask”和“singleInstance”模式,Activity总是位于任务的根部。正是它们定义了一个任务,所以它们绝不会被加载到其它任务之中。
v Activity是否可以存在多个实例。“standard”或者“singleTop”Activity可以被多次实例化。它们可以属于多个任务,而一个任务也可以拥有相同Activity的多个实例。
相反,对于“singleTask”和“singleInstance”的Activity被限定只能有一个实例。因为这些Activity都是任务的起源,这种限制意味着在一个设备中同一时间只允许存在一个任务实例。
v 实例所在的任务中是否有其他的Activity。“singleInstance”模式的Activity将以唯一的Activity出现在它所在的任务。如果启动了别的Activity,不管是何种启动模式,新的Activity都将加载到其它的任务之中——如同在Intent中设置了FLAG_ACTIVITY_NEW_TASK 标记一样的效果。在其它所有方面,“singleInstance”模式的效果与“singleTask”相同。剩下的三种模式允许一个任务中出现多个Activity。“singleTask”模式的Activity将是任务的根Activity,但它可以启动别的Activity并将它们加入到所在的任务之中。“standard”和“singleTop”模式的Activity可以在堆栈的任意位置出现。
v 是否要加载新的类实例来处理新的Intent。对于默认的”standard“模式,每次都会创建一个新的实例来响应新的Intent,每个实例仅处理一个Intent。对于“singleTop”模式,如果Activity位于目标任务堆栈的最上面,则重用该Activity来处理新的Intent。如果它不是位于堆栈顶部,则不会使用它。而是创建一个新的实例来处理新的Intent并将其加入堆栈。
例如,设想一个任务的堆栈由根Activity A和Activity B、C和位于堆栈顶部的D组成,即堆栈A-B-C-D。一个针对D类型Activity的Intent到达的时候,如果D是默认的“standard”启动模式,则创建并加载一个新的类实例,于是堆栈变为A-B-C-D-D。但是,如果D的启动模式为“singleTop”,则现有的实例D则会处理新的Intent(因为它位于堆栈顶部)而堆栈保持A-B-C-D的结构。
另一方面,如果新到达的Intent是针对B类型的Activity,则无论B的模式是“standard”还是“singleTop”,都会加载一个新的B实例(因为B不位于堆栈的顶部),而堆栈的顺序变为A-B-C-D-B。
如前所述,“singleTask”或“singleInstance”模式的Activity永远不会存在多个实例。所以该实例将处理所有的新Intent。“singleInstance”模式的Activity永远保持在堆栈的顶部(因为它是堆栈中唯一的Activity),因此它一直位于该处理Intent的位置。但是,对于“singleTask”模式的Activity,它的任务堆栈上面可能有,也可能没有其他的Activity。如果有,它就不在能够处理Intent的位置上,则那个Intent被抛弃。(即便在Intent被抛弃的情况下,它的到底仍将使这个任务切换至前台并一直保留。)
当现有的Activity被要求处理新的Intent时,会调用他的onNewIntent()方法并将Intent对象传入。(原始启动Activity的Intent对象可以通过调用getIntent()方法获得。)
请注意,当创建一个新的Activity实例来处理新的Intent时,用户总是可以按下BACK键来回到前面的状态(回到前一个 Activity)。但是当使用现有的Activity来处理新Intent的时候,用户是不能靠按下BACK键回到当这个新Intent抵达之前的状态的。
关于启动模式的更多信息,请参阅AndroidManifest.xml文件。
1.2.3 清理堆栈
如果用户离开一个任务很长一段时间,系统会清理该任务中除了根Activity之外的所有Activity。当用户再次回到这个任务的时候,除了只剩下初始Activity尚存之外,其余都跟用户离开任务的时候一样。这样设计的原因在于:过一段时间之后,用户再次回到一个任务的时候,很可能希望放弃之前所做的工作,开始新的事情。
这些属于默认行为,当然也有一些Activity属性来控制并改变这些行为:
v alwaysRetainTaskState 属性
如果任务的根Activity的此属性设置为“true”,则上述默认行为不会发生。任务将在很长的一段时间内保留堆栈内的所有Activity。
v clearTaskOnLaunch属性
如果任务的根Activity的此属性设置为“true”,则每当用户离开这个任务和返回它的时候,堆栈都会被清空只留下根Activity。换句话说,这是alwaysRetainTaskState的另一个极端。即使是短暂的离开,用户回到任务时,也是它的初始状态。
v finishOnTaskLaunch属性
这个属性与clearTaskOnLaunch属性相似,但它仅作用于单个的Activity,而不是整个的任务。而且它可以使任意Activity都被清理,包括根Activity。当它设置为“true”的时候,此Activity仅做为任务的一部分存在于当前回话中,一旦用户离开并再次回到这个任务,此Activity将不再存在。
还有另外一种方法将Activity从堆栈中强制移除。如果Intent对象包含FLAG_ACTIVITY_CLEAR_TOP标记,而且目标任务的堆栈中已经存在了一个能够响应此Intent的Activity类型的实例,则这个实例之上的所有Activity都将被清理以使它位于堆栈的顶部来响应Intent。如果指定的Activity的启动模式为“standard”,则它本身也会从堆栈中移除,并启动一个新的实例来处理新的Intent。这是因为加载模式为“standard”的Activity总会创建一个新实例来处理新的Intent。
FLAG_ACTIVITY_CLEAR_TOP与FLAG_ACTIVITY_NEW_TASK经常合并使用。这时,这些标记提供了一种方法:定位其它任务中现存的Activity、并将它们置于可以对Intent做出响应的位置。
1.2.4 启动任务
当Activity的Intent Filter的动作指定为“android.intent.action.MAIN”,类型指定为“android.intent.category.LAUNCHER”(前面对Intent Filter一节中已经有一个此类型的示例),它就被设定为任务的入口点。这样的过滤器设置会在应用程序加载器中为此Activity显示一个图标和标签,以供用户加载任务或者在加载之后在任意时间返回到这个任务。
第二个功能相当重要:用户必须可以离开一个任务,并在一段时间后返回它。出于这个考虑,总是会初始化一个新任务的启动模式“singleTask”和“singleInstance”,这两种模式仅能用于指定了MAIN和LAUNCHER过滤器的Activity。例如,如果没指定这样的过滤器将会发生什么呢:用Intent启动了一个“singleTask”的Activity,初始化了一个新任务,用户在这个任务中花费了一些时间来完成工作。然后用户按下HOME键。该任务切换至后台并被主屏幕所覆盖。由于它并没有在应用程序加载器中显示图标,这将导致用户再也无法回到它。
FLAG_ACTIVITY_NEW_TASK标记也可能引起类似的问题。如果此标记使一个Activity启动了一个新任务继而用户按下了HOME键离开了它,则用户必须要有一些方法再次回到这个任务。一些实体(比如通知管理器)总是在外部任务中启动新的Activity,而不是做为它们自己的一部分,所以它们总是将FLAG_ACTIVITY_NEW_TASK标记包含在Intent里面并传递给startActivity()。如果写了一个能被外部实体使用这个标记调用的Activity,我们必须注意要给用户留一个返回这个被外部实体启动的任务的方法。
当不希望让用户再次返回一个Activity时,可以将<activity>元素的finishOnTaskLaunch设置为“true”。参考前面的清理堆栈。