Activity的启动模式
1. Activity的LaunchMode
我们知道,在默认情况下,当我们多次启动同一个Activity
的时候,系统会创建多个实例并把它们一一放入任务栈中,当我们单击back
键,会发现这些Activity
会一一回退。任务栈是一种后进先出
的栈结构,每按一下back
键就会有一个Activity
出栈,直到栈空为止,当栈中无任何Activity
的时候,系统就会回收这个任务栈。
目前有四种启动模式:standard
,singleTop
,singleTask
和singleInstance
。
(1). standard:标准模式
这是系统的默认模式,每次启动一个Activity
都会重新创建一个新的实例,不管这个实例是否已经存在。被创建的实例的生命周期符合典型情况下Activity
的生命周期,如上节所述,它的onCreate
,onStart
,onResume
都会被调用。这是一种典型的多实例实现,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个Activity
,那么这个Activity
就运行在启动它的那个Activity
所在的栈中。比如ActivityA
启动了ActivityB
(B
是标准模式),那么B
就会进入到A
所在的栈中。不知你们发现没有,当我们用ApplicationContext
去启动standard
模式的Activity
的时候,会出现错误。
这是因为standard
模式的Activity
默认会进入启动它的Activity
所属的任务栈中,但是由于非Activity
类型的Context
(如ApplicationContext
)并没有所谓的任务栈,所以这就有问题了。解决这个问题的方法是为待启动Activity
指定FLAG_ACTIVITY_NEW_TASK
标记位,这样启动的时候就会为它创建一个新的任务栈,这个时候待启动Activity
实际上是以singleTask
模式启动的。
(2). singleTop:栈顶复用模式
在这种模式下,如果新Activity
已经位于任务栈的栈顶,那么此Activity
不会被重新创建,同时它的onNewIntent
方法会被回调,通过此方法的参数我们可以取出当前请求的信息,需要注意的是,这个Activity
的onCreate
,onStart
不会被系统调用,因为它并没有发生改变。如果新Activity
的实例已存在但不是位于栈顶,那么新Activity
仍然会重新重建。
(3). singleTask:栈内复用模式
这是一种单实例模式,在这种模式下,只要Activity
在一个栈中存在,那么多次启动此Activity
都不会重新创建实例,和singleTop
一样,系统也会回调其onNewIntent
。具体一点,当一个具有singleTask
模式的Activity
请求启动后,比如ActivityA
,系统首先会寻找是否存在A
想要的任务栈,如果不存在,就重新创建一个任务栈,然后创建A
的实例后把A
放到栈中。如果存在A
所需的任务栈,这时要看A
是否在栈中有实例存在,如果有实例存在,那么系统就会把A
调到栈顶并调用它的onNewIntent
方法,如果实例不存在,就创建A
的实例并把A
压入栈中。
举几个例子:
- 比如目前任务栈
S1
中的情况为ABC
,这个时候Activity D
以singleTask
模式请求启动,其所需要的任务栈为S2
,由于S2
和D
的实例均不存在,所以系统会先创建任务栈S2
,然后再创建D
的实例并将其入栈到S2
.
- 另外一种情况,假设
D
所需的任务栈为S1
,其他情况如上面例子,那么S1
已经存在,所以系统会直接创建D
的实例并将其入栈到S1
。
- 如果
D
所需的任务栈为S1
,并且当前任务栈S1
的情况为ADBC
,根据栈内复用的原则,此时D
不会重新创建,系统会把D
切换到栈顶并调用其onNewIntent
方法,同时由于singleTask
默认具有clearTop
的效果,会导致栈内所有在D
上面的Activity
全部出栈,于是最终S1
中的情况为AD
,这一点比较特殊,在后面还会对此种情况详细地分析。
(4) singleInstance:单实例模式
这是一种加强的singleTask
模式,它除了具有singleTask
模式的所有特性外,还加强了一点,那就是具有此种模式的Activity
只能单独地位于一个任务栈中,换句话说,比如Activity A
是singleInstance
模式,当 A
启动后,系统会为它创建一个新的任务栈,然后A
独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的Activity
,除非这个独特的任务栈被系统销毁了。
这里需要指出一种情况,我们假设目前有2个任务栈,前台任务栈的情况为AB
,而后台任务栈的情况为CD
,这里假设CD
的启动模式均为singleTask
。现在请求启动D
,那么整个后台任务栈都会被切换到前台,这个时候整个后退列表变成了ABCD
。当用户按Back
键的时候,列表中的Activity
会一一出栈。
如下图:
如果不是请求启动
D
而是启动C
,那么情况就不一样了。如下图:
另外一问题是,在
singleTask
启动模式中,多次提到某个Activity
所需的任务栈,什么是Activity
所需要的任务栈呢?这要从一个参数说起:TaskAffinity
,可以翻译为任务相关性。这个参数标识了一个Activity
所需要的任务栈的名字,默认情况下,所有Activity
所需的任务栈的名字为应用的包名。当然,我们可以为每个Activity
都单独指定TaskAffinity
属性,这个属性必须不能和包名相同,否则就相当于没有指定。TaskAffinity
属性主要和singleTask
启动模式或者allowTaskReparenting
属性配对使用,在其他情况下没有意义。另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity
位于暂停状态,用户可以通过切换将后台任务栈再次调到前台。
当TaskAffinity
和singleTask
启动模式配对使用的时候,它是具有该模式的Activity
的目前任务栈的名字,待启动的Activity
会运行在名字和TaskAffinity
相同的任务栈中。
当TaskAffinity
和allowTaskReparenting
结合的时候,这种情况比较复杂,会产生特殊的效果。当一个应用A
启动了应用B
的某个Activity
后,如果这个Activity
的allowTaskReparenting
属性为true
的话,那么当应用B
被启动后,此Activity
会直接从应用A
的任务栈转移到应用B
的任务栈中。这还是很抽象,在具体点,比如现在有两个应用A
和B
,A
启动了B
的一个ActivityC
,然后按Home
键回到桌面,然后再单击B
的桌面图标,这个时候并不是启动了B
的主Activity
,而是重新显示了已经被应用A
启动的ActivityC
,或者说,C
从A
的任务栈转移到了B
的任务栈中。可以这么理解,由于A
启动了C
,这个时候C
只能运行在A
的任务栈中,但是C
属于B
应用,正常情况下,它的TaskAffinity
肯定不可能和A
的任务栈相同(因为包名不同),所以,当B
被启动后,B
会创建自己的任务栈,这个时候系统发现C
原本想要的任务栈已经被创建了,所以就把C
从A
的任务栈中转移过来了。
如何给Activity
指定启动模式呢?有两种方法,第一种是通过AndroidMenifest
为Activity
指定启动模式。
<activity
android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
另一种情况是通过在Intent
中设置标志位来为Activity
指定启动模式:
Intent intent = new Intent();
intent.setClass(MainActivity.class,SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
这两种方式都可以为Activity
指定启动模式,但是两者还是有区别的。首先,优先级上,第二种方式的优先级要高于第一种,当两种同时存在时,以第二种方式为准;其次,上述两种方式在限定范围上有所不同,比如,第一种方式无法直接为Activity
设定FLAG_ACTIVITY_CLEAR_TASK
标识,而第二种方式无法为Activity
指定singleInstance
模式。
standard
和singleTop
都比较好理解,singleInstance
由于其特殊性也好理解,但是关于singleTask
有一种情况需要再说明一下。如上图所示,如果在ActivityB
中请求的不是D
而是C
,那么情况如何呢?答案是任务栈列表变成了ABC
,ActivityD
被直接出栈了。
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:launchMode="standard">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:taskAffinity="com.ryg.task1"
android:launchMode="singleTask"/>
<activity
android:name=".ThirdActivity"
android:taskAffinity="com.ryg.task1"
android:launchMode="singleTask">
</activity>
我们将SecondActivity
和ThirdActivity
都设成singleTask
并指定它们的taskAffinity
属性为"com.ryg.task1"
,注意这个taskAffinity
属性的值为字符串,且中间必须含有包名分隔符"."
。然后做如下操作,在MainActivity
中单击按钮启动SecondActivity
,在SecondActivity
中单击按钮启动ThirdActivity
,在ThirdActivity
中单击按钮又启动MainActivity
,最后再在MainActivity
中单击按钮启动SecondActivity
,现在按2次back
键,然后看到的是哪个Activity?
答案是回到桌面。
首先,从理论上分析这个问题,先假设MainActivity
为A
,SecondActivity
为B
,ThirdActivity
为C
。我们知道A
为standard
模式,按照规定,A
的taskAffinity
值继承自Application
的taskAffinity
,而Application
的默认taskAffinity
为包名,所以A
的taskAffinity
为包名。由于我们在XML
中为B
和C
指定了taskAffinity
和启动模式,所以B
和C
是singleTask
模式且有相同的taskAffinity
值“com.ryg.task1”
。A
启动B
的时候,按照singleTask
的规则,这个时候需要为B
重新创建一个任务栈"com.ryg.task1"
。B
再启动C
,按照singleTask
的规则,由于C
所需的任务栈(和B
为同一任务栈)已经被B
创建,所以无需在创建新的任务栈,这个时候系统只是创建C
的实例后将C
入栈了。接着C
在启动A
,A
是standard
模式,所以系统会为它创建一个新的实例并将它加到启动它的那个Activity
的任务栈,由于是C
启动了A
,所以A
会进入C
的任务栈中并位于栈顶。这个时候已经有两个任务栈了,一个是名字为包名的任务栈,里面只有A
,另一个是名字为"com.ryg.task1"
的任务栈,里面的Activity
为BCA
。接下来,A
再启动B
,由于B
是singleTask
,B
需要回到任务栈的栈顶,由于栈的工作模式为“后进新出”
,B
想要回到栈顶,只能是CA
出栈。所以,到这里就很好理解了,如果再按back
键,B
就出栈了,B
所在的任务栈已经不存在了,这个时候只能是回到后台任务栈,并把A
显示出来。注意这个A
是后台任务栈的A
,不是"com.ryg.task1"
任务栈的A
,接着在继续back
,就回到桌面了。分析到这里,我们得出一条结论,"singleTask"
模式的Activity
切换到栈顶会导致在它之上的栈内的Activity
出栈。
Activity的Flags
Activity
的Flag
有很多,这里主要分析一些比较常用的标记位。标记位的作用很多,有的标记位可以设定Activity
的启动模式,比如FLAG_ACTIVITY_NEW_TASK
和FLAG_ACTIVITY_SINGLE_TOP
等,还有的标记位可以影响Activity
的运行状态,比如FLAG_ACTIVITY_CLEAR_TOP
和FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
等。
大部分情况下,我们不需要为Activity
指定标记位,因此,对于标记位理解即可。在使用标记位的时候,要注意有些标记位是系统内部使用的,应用程序不需要去手动设置这些标记位以防止出现问题。
FLAG_ACTIVITY_NEW_TASK
这个标记位的作用是为Activity
指定singleTask
启动模式,其效果和在XML
中指定该启动模式相同。
FLAG_ACTIVITY_SINGLE_TOP
这个标记位的作用是为Activity
指定singleTop
启动模式,其效果和在XML
中指定该启动模式相同。
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的Activity
,当它启动时,在同一个任务栈中所有位于它上面的Activity
都要出栈。这个标记位一般会和singleTask
启动模式一起出现,在这种情况下,被启动Activity
的实例如果已经存在,那么系统就会调用它的onNewIntent
。如果被启动的Activity
采用standard
模式启动,那么它连同它之上的Activity
都要出栈,系统会创建新的Activity
实例并放入栈顶。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有这个标记位的Activity
不会出现在历史Activity
的列表中,当某些情况下我们不希望用户通过历史列表回到我们的Activity
的时候这个标记比较有用,它等同于在XML
中指定Activity
的属性android:excludeFromRecents="true"
。