本文翻译自android官方文档,结合自己测试,整理如下。
概述
一个应用程序通常包括多个activities,每个activity需要指定特定的功能,有时候需要启动其它activity。如一个邮件应用中可能有一个用于显示内容列表的activity,当用户选择某个邮件内容时,则会打开一个新的activity,该activity用于显示邮件具体内容。
一个Activity甚至可以去启动设备上其它应用程序的Activity。例如,如果我们的程序想要发送一封邮件,我们可以通过定义一个带有send
动作以及邮件地址和内容等信息的Intent对象,利用该对象就能响应满足要求的所有activities,即使不在一个应用程序中。当处理完邮件后,仍然可以返回到activity中。因此用户感觉到的是我们的程序有发送邮件的功能。即使activities来自于不同应用程序,Android系统仍然可以利用相同的任务(task)将他们结合到一起。
任务是activities(小写表示Activity实例,下面也都是这个意思)的集合,这些activities用于完成用户的某项特定任务。这些activities被安排到Back栈中,且activities以打开的顺序在栈中进行排列,该栈满足”后进先出原则”。
大多数任务是从设备的Home屏幕上启动的,当用户在Home屏幕上点击程序图标时,该程序的任务就会来到前台。若该程序最近没有被启动的话,系统就会创建一个新的任务,并且将该程序的主activity放入到Back栈的栈顶位置,作为根activity。
当前activity启动另一个activity时,新的activity入栈,处于栈顶位置,并获得用户焦点。前一个activity仍然保留在Back栈中,但进入stopped状态(此时系统保存该activity的UI状态信息)。当用户按下Back键时,处于栈顶位置的activity出栈(即该activity被销毁),然后先前的activity则到达栈顶位置,即重新获得用户焦点(恢复之前的状态),进入resumed状态。在Back栈中的activities的顺序绝对不会发生变化,只能从栈顶进出——当启动新的activity时入栈;当使用Back按钮时出栈,即Back栈满足后进先出原则。下图详细地展示了我们上面的描述:
如果用户一直按Back键,则Back栈中的activities会依次被移除,直到返回到Home屏幕(或者返回到启动该任务的activity)。当Back栈为空时,则该栈对应的任务也就随之不存在了。
任务是一个内部紧密结合的单元,即它能够在新建时转移到前台,也能在按下Home键返回到后台。当任务处于后台时,Back栈中所有的activities都进入stopped状态,但是该Back栈还是保存完整不变的——只是该任务失去了用户焦点。如下图所示(Task B接到用户请求显示在前台,而Task A则进入后台,等待用户再次请求显示):
后台任务可以切换到前台,因此用户就会看到之前离开的任务。例如,当前任务A的栈中有三个activities,当用户按Home键后,任务A进入后台,然后启动另外一个程序时,系统会为该程序创建一个新的任务B。当用户和该程序交互完之后,再次按Home键回到屏幕时,任务B也进入了后台。当用户重新打开第一次的程序时,任务A就回到了前台,任务A的Back栈中的三个activities顺序没有发生变化,栈顶activity进入运行状态。之后用户仍然可以通过Home键切换到任务B,或者启动其它任务。这就是Android多任务切换的例子。
注意:同一时刻可以有多个后台任务,但是当系统内存不足时,系统可能销毁某些activities来恢复内存空间。
由于Back栈中的activities排列顺序永远不会改变,如果我们的程序中允许用户从多个activities启动指定的activity的话,那么每次启动的时候都会创建该Activity的实例(而不是将之前的activity移动到栈顶)。这样就可能导致同一个Activity被实例化多次,如下图所示:
在按Back键时,将按照Back栈中的顺序依次显示(每个activity都有自己的UI状态,即使同一个Activity的不同实例,它们的UI状态也不一定相同)。
那么如何才能避免实例被多次创建呢,这一部分将在后续介绍。
我们先总结一下activities和任务的默认情况下的行为模式:
- 当activity A启动activity B时,Activity A进入stopped状态,但系统仍然会保存A的状态信息,例如文本框输入的内容等。如果用户在B中按下Back键,那么A将会重新进入运行状态。
- 当用户通过Home键离开一个任务时,当前activity进入stopped状态,并且该任务进入后台。系统保存Back栈中的所有activities的状态信息。当用户之后重新打开这个程序时,该任务进入到前台,并显示栈顶位置的activity。
- 如果用户按下Back键,当前activity会从Back栈中移除,并系统被销毁。目前处于栈顶位置的activity将进入运行状态。当activity被销毁后,系统不会再保存它的任何状态信息。
- 每个activity都可以被实例化多次,不管是否在一个任务中。
保存activity状态信息
我们知道当activity进入stopped状态后,默认情况下系统保存该activity状态信息。因此,当用户下次激活该activity后,该activity能回到原来的状态。然而我们应该主动去保存这些状态信息,以防stopped状态的activity被系统销毁然后重建。
当activity进入stopped状态后,系统在内存不足的情况下可能完全销毁activity。这种情况下,系统不再为我们的activity保存信息,但是系统仍然知道activity在Back栈中的位置,但是在该activity到达栈顶时系统必须重新创建该activity。为了避免丢失用户之前的信息,我们应该主动保存这些信息(在activity中覆盖onSaveInstanceState()
)。
关于这部分有兴趣的可以参见我之前翻译的文章:解读Android之Activity(1)基础知识
管理任务
Android按照上面的方式管理任务和Back栈,把所有启动的activities都放到一个相同的任务中,Back栈通过后进先出原则来管理。这种方式适合绝大多数应用程序,我们无须关心任务中的activity是怎么样和任务关联以及是怎样存放在Back栈中的。然而,有时我们可能会希望打破这种默认的行为。例如,我们希望在一个新的任务中存放新启动的activity,而不是存放在现有的任务中。或者,当启动一个activity时,我们希望使用Back栈中已经存在的该实例,而不是再创建一个。或者,我们想当用户离开任务后,清除Back栈中除了最底层的activity外的其它所有activity。
这些实现或者其它更多实现,都可以通过在manifest文件中设置<activity>
标签的属性实现,或是在启动activity时配置Intent对象的flag属性实现。
在<activity>
标签中,有以下几个属性可以使用:
taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch
在Intent中,我们可以使用:
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_SINGLE_TOP
在下面的部分中,我们具体讨论以上属性。
注意:仍然要提醒一下,默认的启动方式适合绝大多说程序,若我们修改的话,要确保BACK键以及其它导航回退键的可用性,以及避免和用户希望的行为发生冲突。
定义启动模式
启动模式允许我们定义如何将activity和当前任务关联,我们可以通过以下两种方式来定义:
- 使用manifest文件
当在manifest文件中声明activity时,我们可以指定该activity在启动时如何与任务进行关联。
- 使用Intent属性flag
当调用
startActivity()
时,我们可以在Intent对象中设置flag属性,指定新的activity如何与当前任务进行关联。
例如,如果activity A启动了activity B,B可以定义自己如何与当前任务关联,而A也可以要求B如何与当前任务关联。若B在manifest中已经定义了关联方式,而A也在Intent中设置了flag,则A的请求Intent中的启动方式将覆盖B在manifest中的设置。
注意:有些启动模式在manifest中可以指定,而在intent中则不能使用。同样,有些启动模式在intent中可以使用,而在manifest中则不能使用。下面具体介绍。
使用manifest文件
当在manifest文件中声明activity时,我们可以通过<activity>
标签的launchMode
属性设置该activity的启动模式,有以下四种取值:
standard
默认模式。每次启动该activity时系统都会创建一个新的实例,并且总会把它放入到当前的任务当中。这种启动模式的Activity可以被实例化多次,一个任务中也可以包含多个该模式Activity的实例。
singleTop
栈顶单一模式。如果要启动的activity在当前Back栈的栈顶位置,系统会将Intent对象传递给该activity的
onNewIntent()
方法(之后调用onResume()
再次启动activity),而不会创建新的实例。这种启动模式的Activity可以被实例化多次,一个任务中也可以包含多个该模式Activity的实例(只有当栈顶位置不是该activity时)。注意:这种模式的activity无法通过BACK键返回到之前的状态(调用
onNewIntent()
之前的状态)。singleTask
任务单一模式。系统会创建一个新的任务,并将该activity放入新任务的Back栈的栈底位置。但是若现有的任务(包括当前任务在内的所有任务,关于为什么该activity可能在当前任务中这个在
taskAffinity
属性中介绍)中已经存在该activity,则系统直接调用该activity的onNewIntent()
(之后调用onRestart()等方法再次启动activity)(在该activity之上的所有activity都会出栈,然后让该activity处于栈顶位置),而不是创建新的activity。该模式的Activity在一个任务中只会存在一个实例。然后在该activity之后启动的默认都是放在和该activity相同的back栈中。这种模式会受到
taskAffinity
属性的影响,在taskAffinity
有介绍。例如,Android系统内置的浏览器程序声明自己浏览网页的activity始终在自己的任务中打开,也就是通过在
<activity>
标签中设置singleTask
启动模式来实现的。这意味着当我们的程序打开该浏览器时,它的activity不会放到我们当前的任务中,而是会启动一个新的任务;如果浏览器已经在后台中存在一个任务,则直接把这个任务切换到前台来处理Intent对象。singleInstance
单一实例模式。和
singleTask
相同,除了一点:系统不会向含有该模式activity的任务中再添加其它activities,即该任务只有一个activity。通过该activity打开的其它activities会被放在另外的任务中。这种模式不会受到taskAffinity
属性的影响
无论activity是否在一个新任务中启动,还是在当前任务中启动,BACK键都会回到之前的activity中的,这个之前的activity
并不一定是启动当前activity的那个activity。而应该是若该Back栈中的前一个,若Back栈中不存在的话才返回到启动当前activity的那个activity。
例如activity的启动模式是singleTask
的话,并且该activity处于一个后台任务中,则整个任务都会切换到前台。此时按BACK键的话,则会返回到当前任务的Back栈中的下一个activity(如果存在),而不是返回到启动该activity的activity中,如下图描述:
更多内容可以参考activity标签介绍
注意:通过launchMode
设置activity的启动模式会被Intent属性flags覆盖。这一部分后续介绍。
使用Intent属性flags
在startActivity()
中可以通过设置传递的Intent对象的flags设置将要启动的activity的启动模式,flag有如下取值:
FLAG_ACTIVITY_NEW_TASK
在新的任务中启动activity。若现有的其它任务中存在该activity,则直接将任务切换到前台,并将Intent对象传递给该activity的
onNewIntent()
。这种模式和
singleTask
一样,而且也受到其它条件限制。FLAG_ACTIVITY_SINGLE_TOP
若启动的activity在Back栈的栈顶位置,直接调用该activity的
onNewIntent()
。这种模式和
singleTop
一样。FLAG_ACTIVITY_CLEAR_TOP
若启动的activity在当前任务中存在,则该activity上面所有的activities全部出栈,Intent对象传递给该activity的
onNewIntent()
来处理,而不是再创建activity。这种模式在launchMode中没有对应的。
FLAG_ACTIVITY_CLEAR_TOP
更多情况下是和FLAG_ACTIVITY_NEW_TASK
结合使用。若使用这种结合,则可以定位另一个任务中存在的activity,并将其上面的所有全部出栈。但是FLAG_ACTIVITY_NEW_TASK
自己也有这个功能,不知道这里什么意思,,,,注意:若在该flag下启动的activity是
standard
模式,则不仅在该activity之上的activity会出栈,它自己也会出栈,并重新创建一个新的activity处理传入的Intent对象。这是因为在standard
模式下,总是创建一个新的activity来处理Intent对象。
处理affinities
affinity表示activity更愿意依附哪个任务,在默认情况下,同一个应用程序中的所有activities都有相同的affinity。因此同一个应用程序中的activities更愿意在相同的任务中。我们也可以去改变activity的affinity,不同程序中的activities可以有相同的affinity,同一个程序中activities的affinity也可以不同。
我们可以通过<activity>
标签的taskAffinity
属性修改affinity值。
taskAffinity值为字符串,你可以指定成任意的值(字符串中至少要包含一个.
,否则会出现INSTALL_PARSE_FAILED_MANIFEST_MALFORMED
错误),但不能和应用程序的包名相同,因为系统会使用包名来作为默认的affinity值。
affinity主要有以下两种应用环境:
- 当启动activity的Intent的对象包含
FLAG_ACTIVITY_NEW_TASK
(或者在manifest文件中声明启动模式为singleTask
)时默认情况下,当调用
startActivity()
启动activity时,是将该activity放到当前任务中。上面我们介绍了当Intent的对象包含FLAG_ACTIVITY_NEW_TASK
或者在manifest文件中声明启动模式为singleTask
时,系统会创建一个新的任务来存放该activity或者直接使用现有其它任务中的该activity,但是这种情况不一定发生。当现有的任务(包括当前任务)中存在和该activity相同的affinity时,则就会将该activity放入到和它有相同affinity的任务中。若没有的话,才会创建新的任务存放该activity。这种方式会创建一个新的任务管理activity,当用户按HOME键后,我们必须保证,用户可以通过其它导航回到该任务中。若我们想要调用外部任务的activity必须要设置activity的启动方式为这两种方式之一,同时确保用户有独立的方式能够回到启动的任务中,例如通过启动图标等。
- 当设置activity的
allowTaskReparenting
属性为true
时在这种情况下,activity可以从启动它的任务中转移到和该activity有相同affinity值的任务中去,前提是和该activity有相同affinity值的任务切换到了前台。
例如在天气预报程序中有一个activity用于显示天气情况的,该activity和该程序的所有activities有相同的affinity值,且
allowTaskReparenting
为true。当我们自己的应用程序启动该的activity时,此时该activity和我们的程序在同一个任务中。但是当天气预报程序切换到前台时,该ctivity会被转移到天气预报程序的任务中,并显示。
注意:
- 当一个`.apk文件包含多个”应用程序”时,我们应该设置activities的affinity值,用于关联具体的”应用程序”。
- 不同affinity值的activity可以放在同一个back栈中。例如:
singleTask
的activity在启动standard
和非singleTop
的activity时,就会将启动的activity放在同一个back栈中。
清除Back栈
如果用户长时间离开一个任务的话,系统会将该任务中除最底层activity之外的所有activity清除。当用户返回到该任务时,只有最底层的activity被保存。这是系统默认行为,这是因为过了这么长的时间,用户很可能早就忘了当时正在做的事情,重新又回来很可能希望做其它的事情。
可以在<activity>
标签中设置以下几种属性来改变:
alwaysRetainTaskState
若将最底层的activity的这个属性设置为true
,则任务会在Back栈中保留所有的activities,即使过了很长时间。
clearTaskOnLaunch
若将最底层的activity的这个属性设置为true
,则只要用户离开该任务,系统都会将最底层Activity之上的所有activities清除,无论离开多长时间。换句话说,就是和alwaysRetainTaskState
相反,用户每次返回该任务都是初始状态,即使用户刚刚离开。
finishOnTaskLaunch
这个属性和clearTaskOnLaunch
相似,但是它是作用在单个activity上,而不是整个任务上。当某个activity的这个属性设置为true
,若用户离开了当前任务,该activity就会被销毁,即使是最底层activity。
启动一个任务
我们可以在<activity>
标签中通过下列设置作为一个任务入口:
<activity ... >
<intent-filter ... >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...
</activity>
带有该intent filter的activity的图标和名字将是该应用程序的图标和名字(Home屏幕上的)。该图标能够启动或回到应用程序的入口activity。
我们必须要保证用户在离开任务后,在之后能够通过该图标再次启动任务。因为这个原因,singleTask
和singleInstance
的activities必须要是程序的入口activity,否则的话,用户在离开任务后无法再回到该任务(因为程序的入口任务(通过图标启动的)不是该任务),因此就会造成数据丢失。当然也并非完全不能找到该任务,我们也可以在Overview Screen检索到,关于Overview Screen将后续翻译。