在android应用开发中,界面的引导和跳转是值得深入研究的重要内容。在开发中,与界面跳转联系比较紧密的概念是Task(任务)和Back Stack(回退栈)。activity的启动模式会影响Task和Back Stack的状态,进而影响用户体验。除了启动模式(LaunchMode)之外,Intent类中定义的一些标志(以FLAG_ACTIVITY_开头)也会影响Task和Back Stack的状态。其中也涉及到activity的一个重要属性taskAffinity和Intent中的标志(setFlag)之一FLAG_ACTIVITY_NEW_TASK。关于Intent中其他标志位的具体用法会在另一篇文章中介绍。
Task是一个存在于Framework层的概念,容易与它混淆的有Application(应用)和Process(进程)。在开始介绍Activity的启动模式的使用之前,首先对这些概念做一个简单的说明和区分。
<pre name="code" class="html"><?xml version="1.0" encoding="utf-8"?> <manifest android:versionCode="1" android:versionName="1" xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.myapp"> <application android:label="@string/app_name"> <activity android:name=".MyActivity" android:label="@string/app_nam"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name=".MyReceiver"/> <provider android:name=".MyProvider"/> <service android:name=".MyService"/> </application> </manifest>
由此可见,application是由四大组件组成的。在app安装时,系统会读取manifest的信息,将所有的组件解析出来,以便在运行时对组件进行实例化和调度。
<pre name="code" class="html"> <activity android:name=".MyActivity" android:label="@string/app_nam" android:process=":remote"> </activity>
这样的话这个activity会运行在一个独立的进程中。
<activity android:name=".app.InterstitialMessageActivity" android:label="@string/interstitial_label" android:theme="@style/Theme.Dialog" <span style="color:#FF0000;">android:launchMode="singleTask</span>" </activity>
标准启动模式,也是activity的默认启动模式。在这种模式下启动的activity可以被多次实例化,即在同一个任务中可以存在多个activity的实例,每个实例都会处理一个Intent对象。如果Activity A的启动模式为standard,并且A已经启动,在A中再次启动Activity A,即调用startActivity(new Intent(this,A.class)),会在A的上面再次启动一个A的实例,即当前的桟中的状态为A-->A。
如果一个以singleTop模式启动的activity的实例已经存在于任务桟的桟顶,那么再启动这个Activity时,不会创建新的实例,而是重用位于栈顶的那个实例,并且会调用该实例的onNewIntent()方法将Intent对象传递到这个实例中。举例来说,如果A的启动模式为singleTop,并且A的一个实例已经存在于栈顶中,那么再调用startActivity(new Intent(this,A.class))启动A时,不会再次创建A的实例,而是重用原来的实例,并且调用原来实例的onNewIntent()方法。这是任务桟中还是这有一个A的实例。
如果以singleTop模式启动的activity的一个实例已经存在与任务桟中,但是不在桟顶,那么它的行为和standard模式相同,也会创建多个实例。
谷歌的官方文档上称,如果一个activity的启动模式为singleTask,那么系统总会在一个新任务的最底部(root)启动这个activity,并且被这个activity启动的其他activity会和该activity同时存在于这个新任务中。如果系统中已经存在这样的一个activity则会重用这个实例,并且调用他的onNewIntent()方法。即,这样的一个activity在系统中只会存在一个实例。其实官方文档中的这种说法并不准确,启动模式为singleTask的activity并不会总是开启一个新的任务。详情请参考 解开Android应用程序组件Activity的"singleTask"之谜,在本文后面也会通过示例来进行验证。
总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其他activity会自动运行于另一个任务中。当再次启动该activity的实例时,会重用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。和singleTask相同,同一时刻在系统中只会存在一个这样的Activity实例。
下面我们对singletask和singeInstance进行验证。
1.验证启动singleTask模式的activity时在什么情况下会创建新的任务(task):
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.jin.cai.wei.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name="com.jin.cai.wei.OneActivity" <span style="color:#FF6666;">android:launchMode="singleTask"</span>></activity> <activity android:name="com.jin.cai.wei.SecondActivity"></activity> </application>
MainActivity--------->oneActivity--------->SecondActivity--------->oneActivity
getTaskId() getTaskId() getTaskId() getTaskId()
结论:由以上的执行顺序和Log结果可知,MainActivity、OneActivity和ScondActivity都是启动在同一个任务中的。 1.当重新启动oneActivity时,Log还是原来的结果,由此可知,在启动一个singleTask的Activity实例时,如果系统中已经存在这样一个实例,就会将这个实例调度到任务栈的栈顶,并清除它当前所在任务中位于它上面的所有的activity。 2. 其实,把启动模式设置为singleTask,framework在启动该activity时只会把它标示为可以在一个新任务中启动,至于是否真的在一个新任务中启动,还要受其他条件的限制。
现在在OneActivity增加一个taskAffinity属性,如下所示:
<activity android:name="com.jin.cai.wei.OneActivity" android:launchMode="singleTask" <span style="color:#FF6666;"> android:taskAffinity="<span style="color:#FF6666;">com.jin.cai.wei.one</span>"</span>></activity>
结论:由以上的执行Log结果可知,当OneActivity添加taskAffinity属性之后,OneActivity和ScondActivit则启动在另外一个任务中。其实, taskAffinity表示一个任务,这个任务就是当前activity所在的任务;在概念上,具有相同的affinity的activity(即设置了相同taskAffinity属性的activity)属于同一个任务。 默认情况下,一个应用中的所有activity具有相同的taskAffinity,即应用程序的包名。我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可以把不同的应用中的activity的taskAffinity设置成相同的值,或者为一个activity的taskAffinity设置一个空字符串,表明这个activity不属于任何task。
注:FLAG_ACTIVITY_NEW_TASK和singleTask作用相同,当启动模式为singleTask时,framework会将它的启动标志设为FLAG_ACTIVITY_NEW_TASK,framework会检索是否已经存在了一个affinity为 com.jin.cai.wei.one的任务(即一个TaskRecord对象)。
framework中的判定过程如下:
- 在MainActivity启动OneActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
- 然后获得OneActivity的taskAffinity,即为包名com.jin.cai.wei.one的任务
- 检查是否已经存在一个taskAffinity为com.jin.cai.wei.one的任务,如果已经存在,则检索在这个任务中是否存在一个OneActivity的实例,如果存在OneActivity实例,直接重用这个任务中的OneActivity实例,将这个任务调到前台,清除位于OneActivity上面的所有Activity,显示OneActivity,并调用OneActivity的onNewIntent();如果发现不存在,创建一个新的taskAffinity为com.jin.cai.wei.one的任务,并且将OneActivity启动到这个新的任务中
其实framework中对任务和activity‘的调度是很复杂的,尤其是把启动模式设为singleTask或者以FLAG_ACTIVITY_NEW_TASK标志启动时。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK时,要仔细测试应用程序。这也是官方文档上的建议。
2.实例验证两个不同app,分别设置两个Activity为singleTask模式,并且将taskAffinity设成相同,测试taskAffinity相同的Activity是否在同一个task中:
我们在上面的测试项目“Test1”的基础上,再新建一个项目“Test2”,在Test2的OtherActivity上添加与Test1的taskAffinity相同,xml清单如下:
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.jin.cai.wei.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name="com.jin.cai.wei.OtherActivity" android:launchMode="singleTask" <span style="color:#FF6666;">android:taskAffinity="com.jin.cai.wei.one<span style="color:#000000;">"</span></span>> </activity> </application>执行以下操作:启动Test1,在它的MianActivity中点击按钮开启OneActivity,由上面的介绍可知OneActivity是运行在一个新任务中的,这个任务就是 com.jin.cai.wei.one。然后按Home键回到桌面,再启动Test2,进入OtherActivity,会自动启动新的任务。那么现在一共有三个任务,Test1的MianActivity和OneActivity分别占用一个任务Test2的MainActivity也占用一个任务。那么这个OtherActivity是在哪个任务中呢?
由此可见,Test1的OneActivity和Test2的OtherActivity是在同一任务中的。而 com.jin.cai.wei.one任务中的两个activity属于不同的应用,并且运行在不同的进程中,这也说明了一个问题: 任务(Task)不仅可以跨应用(Application),还可以跨进程(Process)。
3.singleInstance的行为:
根据上面的讲解,并且参考谷歌官方文档,singleInstance的特点可以归结为以下三条:
- 以singleInstance模式启动的Activity具有全局唯一性,即整个系统中只会存在一个这样的实例。(与singletask的差异)。
- 以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中。(官方文档上的描述为,singleInstance模式的Activity不允许其他Activity和它共存在一个任务中。与singletask的差异)
- 被singleInstance模式的Activity开启的其他activity,能够开启一个新任务,但不一定开启新的任务,也可能在已有的一个任务中开启。(与singletask的共同点)
由上述可知,Task是Android Framework中的一个概念,Task是由一系列相关的Activity组成的,是一组相关Activity的集合。Task是以栈的形式来管理的。我们在操作应用时,会涉及界面的跳转。其实在对界面进行跳转时,Android Framework既能在同一个任务中对Activity进行调度,也能以Task为单位进行整体调度。在启动模式为standard或singleTop时,一般是在同一个任务中对Activity进行调度,而在启动模式为singleTask或singleInstance是,一般会对Task进行整体调度。
对Task进行整体调度包括以下操作:
- 按Home键,将之前的任务切换到后台。
- 长按Home键,会显示出最近执行过的任务列表。
- 在Launcher或HomeScreen点击图标打开应用时,开启一个新任务,或者是将已有的任务调度到前台。
- 当启动singleTask模式的Activity时,会在系统中搜寻是否已经存在一个合适的任务(有已建taskAffinity的任务),1.若存在,则会将这个任务调度到前台以重用这个任务。a.如果这个任务中已经存在一个要启动的Activity的实例,则清除这个实例之上的所有Activity,将这个实例显示给用户;b.如果这个已存在的任务中不存在一个要启动的Activity的实例,则在这个任务的顶端启动一个实例。2.若这个任务不存在,则会启动一个新的任务,在这个新的任务中启动这个singleTask模式的Activity的一个实例。
- 启动singleInstance的Activity时,会在系统中搜寻是否已经存在一个这个Activity的实例,如果存在,会将这个实例所在的任务调度到前台,重用这个Activity的实例(该任务中只有这一个Activity),如果不存在,会开启一个新任务,并在这个新任务中启动这个singleInstance模式的Activity的一个实例。