在android应用开发中,打造良好的用户体验是非常重要的。而在用户体验中,界面的引导和跳转是值得深入研究的重要内容。在开发中,与界面跳转联系比较紧密的概念是Task(任务)和Back Stack(回退栈)。activity的启动模式会影响Task和Back Stack的状态,进而影响用户体验。除了启动模式之外,Intent类中定义的一些标志(以FLAG_ACTIVITY_开头)也会影响Task和Back Stack的状态。在这篇文章中主要对四种启动模式进行分析和验证,其中涉及到activity的一个重要属性taskAffinity和Intent中的标志之一FLAG_ACTIVITY_NEW_TASK。关于Intent中其他标志位的具体用法会在另一篇文章中介绍。
Task是一个存在于Framework层的概念,容易与它混淆的有Application(应用)和Process(进程)。在开始介绍Activity的启动模式的使用之前,首先对这些概念做一个简单的说明和区分。
一 Application,Task和Process的区别与联系
Application翻译成中文时一般称为“应用”或“应用程序”,在android中,总体来说一个应用就是一组组件的集合。众所周知,android是在应用层组件化程度非常高的系统,android开发的第一课就是学习android的四大组件。当我们写完了多个组件,并且在manifest文件中注册了这些组件之后,把这些组件和组件使用到的资源打包成apk,我们就可以说完成了一个application。application和组件的关系可以在manifest文件中清晰地体现出来。如下所示:
由此可见,application是由四大组件组成的。在app安装时,系统会读取manifest的信息,将所有的组件解析出来,以便在运行时对组件进行实例化和调度。而task是在程序运行时,只针对activity的概念。说白了,task是一组相互关联的activity的集合,它是存在于framework层的一个概念,控制界面的跳转和返回。这个task存在于一个称为back stack的数据结构中,也就是说,framework是以栈的形式管理用户开启的activity。这个栈的基本行为是,当用户在多个activity之间跳转时,执行压栈操作,当用户按返回键时,执行出栈操作。举例来说,如果应用程序中存在A,B,C三个activity,当用户在Launcher或Home Screen点击应用程序图标时,启动主Activity A,接着A开启B,B开启C,这时栈中有三个Activity,并且这三个Activity默认在同一个任务(task)中,当用户按返回时,弹出C,栈中只剩A和B,再按返回键,弹出B,栈中只剩A,再继续按返回键,弹出A,任务被移除。如下图所示:
Task是可以跨应用的,这正是task存在的一个重要原因。有的Activity,虽然不在一个app中,但是为了保持用户操作的连贯性,把他们放在同一个任务中。例如,在我们的应用中的一个Activity A中点击发送邮件,会启动邮件程序的一个Activity B来发送邮件,这两个activity是存在于不同app中,但是被系统放到了一个程序中,这样当发送完邮件后,用户按back键返回,可以返回原来的Activity A中,这样就确保了用户体验。
说完了application和task,最后介绍process。Process一般翻译成进程,进程是操作系统内核中的一个概念,表示只接受内核调度的执行单位。在应用程序的角度看,我们用java编写的应用程序,运行在dalvik虚拟机中,可以认为一个运行中的dalvik虚拟机实例占有一个进程,所以,在默认情况下,一个应用程序的所有组件运行在同一个进程中。但是这种情况也有例外,即应用程序中的不同组件可以运行在不同的进程中,只需要在manifest中用process属性指定组件所运行的进程的名字,如下所示:
这样的话,这个activity就会运行在一个单独的进程中。
二 Activity四中启动模型详解
activity有四种启动模式,分别是:standard、singleTop、singleTask、singleIntance,如何要使用这四种启动方式,就必须在manifest文件中进行注册,如下所示:
同样,在Intent类中定义了许多与Activity启动或者调度有关的标志,
Standard
标准启动模式,也是activity的默认启动模式。在这种模式下启动的activity可以被多次实例化,即在同一个任务中可以存在多个activity的实例,每个实例都会处理一个Intent对象。如果Activity A的启动模式为standard,并且A已经启动,在A中再次启动Activity A.即调用startActivity(new Intent(this,A.class)),会在A的上面再次启动一个A的实例,即当前的栈中状态为A->A。
SingleTop
如果一个以singleTop模式启动的activity的实例已经存在于任务栈的栈顶,那么再启动这个Activity时,不会创建新的实例,而是重用位于栈顶的那个实例,并且会调用该实例的onNewIntent()方法将Intent对象传递到这个实例中。举例来说,如果A的启动模式为SingleTop,并且A的一个实例已经存在于栈顶中,那么再调用startActivity(new Intent(this,A.class));启动A时,不会创建A的实例,而是重用原来的实例,并且调用原来实例的onNewIntent()方法。这时任务栈中还是只有A的一个实例。
SingleTask
虽然官方文档说:如果一个activity的启动模式为singleTask,那么系统总会在一个新任务的最底部(root)启动这个activity并且被这个activity启动的其他activity会和该activity同时存在于这个新任务栈中,诶过系统中已经存在这样的一个activity则会重用这个实例,并且调用它的onNewIntent()方法。即这样的一个activity在系统中只会存在一个实例。但是官方文档中的这种说法是不正确的,启动模式为singleTask的activity并不会总是开启一个新的任务。稍后进行验证详解。
SingleIntance
总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其他activity会自动运行于另一个任务中。当再次启动该activity的实例看,会重用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。和singleTask相同,同一时刻在系统中只会存在一个这样的activity实例。
三.实例验证singleTask启动模式
上面的四中启动模式都已经介绍完毕,为了加强理解,下面进行验证,由于singleTop和standard比较简单,所以就不再介绍,下面主要讲解singleTask和singleInstance。
验证启动singleTask模式的Activity时是否会创建新任务
创建android工程:TestLaunchMode,里面有三个Activity,MainActivity、SecondActivity、ThirdActivity,下面是Manifest文件:
从上面可以看出:MainActivity和ThirdActivity的启动方式是standard模式,但是SecondActivity的启动方式是SingleTask模式。三个界面之间就是主界面启动第二个界面,第二个界面启动第三个界面,很简单。
以下是三个Activity的主要代码:
MainActivity:
package com.example.testlaunchermode;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity {
private static final String ACTIVITY_NAME="MainActivity";
private static final String LOG_TAG="****";
private Button btn_next;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_next=(Button) findViewById(R.id.btn_next);
btn_next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
});
int id=getTaskId();
Log.i(LOG_TAG, ACTIVITY_NAME+"所在的任务id为:"+id);
}
}
SecondActivity:
package com.example.testlaunchermode;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class SecondActivity extends Activity {
private static final String ACTIVITY_NAME="SecondActivity";
private static final String LOG_TAG="****";
private Button btn_next;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
btn_next=(Button) findViewById(R.id.btn_next);
btn_next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(SecondActivity.this, ThirdActivity.class);
startActivity(intent);
}
});
int id=getTaskId();
Log.i(LOG_TAG, ACTIVITY_NAME+"所在的任务id为:"+id);
}
}
ThirdActivity:
package com.example.testlaunchermode;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class ThirdActivity extends Activity {
private static final String ACTIVITY_NAME="ThirdActivity";
private static final String LOG_TAG="****";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
int id=getTaskId();
Log.i(LOG_TAG, ACTIVITY_NAME+"所在的任务id为:"+id);
}
}
以上是运行界面和主要代码:实现很简单,就是每一个界面都有一个按钮,启动另一个界面。下面是输出Log:
按照官方文档的说法,SecondActivity应该在一个新的任务栈中,但是从上面的Log信息可以看出他们两个的TaskId是一样的,接下来我们继续在命令行执行adb shell dumpsys activity,发现:
由此可以看出它跟官方文档描述的是不一样,MainActivity和SecondActivity启动是在同一个任务中,其实将启动模式设置成singleTask,framework在启动该activity时只是会把它标记为可以在一个新的任务中启动,但是是否在一个新的任务中启动还要受到其他条件的制约。现在在SecondActivity配置中添加TaskAffinity属性,如下所示:
继续运行一次,将会看到:
1.taskAffinity表示当前Activity具有亲和力的一个任务,可以这样理解,这个taskAffinity表示一个任务,这个任务就是当前的activity所在的任务。
2.在概念上,具有相同的affinity的activity(即设置了相同taskAffinity属性的activity)属于同一个任务。
3.一个任务的affinity决定了这个任务的根activity的taskAffinity。
4.这个属性决定两件事:当activity被reparent时,它可以被reparent哪个任务中;当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任 务中。(这个可以结合
5.默认情况下,一个应用中的所有activity具有相同的taskAffinity,即应用程序的包名,我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可 以把不同的应用中的activity的taskAffinity设置成相同的值。
6.为一个activity的taskAffinity设置一个空字符串,表明这个activity不属于任何task。
这就可以解释上面示例中的现象了,由第五条可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity为“com.example.testlaunchermode”而SecondActivity的taskAffinity为“com.example.testlaunchermode.second”。根据上面的第四条,taskAffinity可以影响activity以FLAG_ACTIVITY_NEW_TASK启动时,它会被启动到哪一个任务中,这句话的意思是,当新启动的activity(SecondActivity)是以FLAG_ACTIVITY_NEW_TASK标志启动时(可以认为FLAG_ACTIVITY_NEW_TASK和singleTask作用相同,当启动模式是singleTask时,framework会将它的启动标志设为FLAG_ACTIVITY_NEW_TASK),framwork会检索是否已经存在了一个affinity为com.example.testlaunchermode.second的任务(即TaskRecord对象)
如果存在这样的一个任务,则检查在这个任务中是否已经有了一个SecondActivity的实例
如果已经存在一个SecondActivity的实例,则会重用这个任务和任务中的SecondActivity实例,将这个任务调到前台,清除位于SecondActivity上面的所有 的Activity,显示SecondActivity,并调用SecondActivity的onNewIntent();
如果不存在一个SecondActivity的实例,会在这个任务中创建SecondActivity的实例,并调用onCreate()方法;
如果不存在这样的一个任务,会创建一个新的affinity为“com.example.testlaunchermode.second”的任务,并且将SecondActivity启动到这个新的任务中。
上面讨论了设置taskAffinity属性的情况,如果SecondActivity只设置了启动模式为SingTask,而不设置taskAffinity,即三个Activity的taskAffinity相同,都为应用的包名,那么SecondActivity是不会开启一个新的任务的,frameWork的判定如下:
1.MainActivity启动SecondActivity时,发现启动模式为SingleTask,那么设定它的启动标志为FLAG_ACTIVITY_NEW_TASK
2.然后获得SecondActivity的taskAffinity,即包名com.example.testlaunchermode
3.检查是否已经存在一个affinity为com.example.testlaunchermode的任务,这个任务是存在的,就是MainActivity所在的任务,这个任务是在启动MainActivity的时候开启的
4.既然已经存在这样的任务,就检索在这个任务中是否存在一个SecondActivity的实例,发现不存在
5.在这个已经存在的任务中创建SecondActivity的实例
为了做一个清除的比较,列出SecondActivity'启动模式设成singleTask,并且taskAffinity设为“com.example.testlaunchermode.second”时的启动过程
1.MainActivity启动SecondActivity时,发现启动模式为SingleTask,那么设定它的启动标志为FLAG_ACTIVITY_NEW_TASK
2.然后获取SecondActivity的taskActivity,即com.example.testlaunchermode.second
3.检查是否已经存在一个affinity为com.example.testlaunchermode.second的任务,这个任务不存在
4.创建一个新的affinity为com.example.testlaunchermode.second的任务,并将SecondActivity启动到这个新的任务中
其实framework中对activity和任务的调度是非常复杂的,尤其是把启动模式设置成singleTask或者以FLAG_ACTIVITY_NEW_TASK标志启动时。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK时,要仔细的测试应用程序。
实例验证将两个不同app中的不同的singleTask模式的Activity的taskAffinity设成相同
官方文档中提到,可以将不同应用的中的activity的taskAffinity设置成相同的值,这样的话这两个activity虽然不在同一个应用中,却会在运行时分配到同一个任务栈中,下面对此进行验证,创建一个新工程,TestLaunchermode2,里面有两个Activity,一个是MainActivity,另一个是OtherActivity,在MainActivity中点击按钮会启动OtherActivity,改程序的界面和上一个类似,代码也类似,在此列出清单文件
可以看到OtherActivity的启动模式被设置成了singleTask,并且taskAffinity属性被设置成为"com.example.testlaunchermode.second"这和
TestLaunchermode中的SecondActivity相同。现在将这两个应用安装到设备上,执行以下操作:启动TestLaunchermode应用,在他的MainActivity中点击按钮开启SecondActivity,由上面的介绍可知SecondActivity是运行在一个新的任务中的,这个任务就是com.example.testlaunchermode.second,然后按住Home键回到Launcher,启动TestLaunchermode2,在启动TestLaunchermode2的入口Activity时,会自动启动新的任务,那么现在就有三个任务了,TestLaunchermode的MainActivity和SecondActivity分别占用一个任务,TestLaunchermode2的MainActivity也占用一个任务,现在在TestLaunchermode2的MainActivity点击按钮启动OtherActivity,那么这个OtherActivity是在哪个任务中呢?通过 adb shell dumpsys activity命令,发现有以下输出:
从上图可以看出任务有三个,TestLaunchermode的SecondActivity和TestLaunchermode2的OtherActivity处于同一个任务中,从上面还可以看出TestLaunchermode和TestLaunchermode2分别开启了两个进程,但是com.example.testlaunchermode.second任务中的两个activity:SecondActivity和OtherActivity属于不同的应用,并且处在两个不同的进程中,这就说明,task不仅可以跨应用而且还可以跨进程
实例验证singleTask在同一个应用中具有唯一性
修改上面的工程TestLaunchermode,添加activity,FourthActivity,其中所有的activity都不设置taskAffinity属性,启动流程为:MainActivity启动SecondActivity,SecondActivity启动ThirdActivity,ThirdActivity启动FourthActivity,FourthActivity启动SecondActivity,SecondActivity启动方式为SingleTask。清单文件如下所示:
此时当点击FourthActivity中的按钮再次启动SecondActivity,注意,此时由于SecondActivity的启动方式是singleTask,则出现结果如下图所示:
此时栈中的状态为MainActivity-->SecondActivity。确实确保了在任务中是唯一的,并且清楚了同一任务中它上面的所有的Activity.那么这个SecondActivity的实例是重用上次已经存在的还是重新启动的呢?可以查看系统Log发现Log并没有改变,这是因为打印Log的语句实在OnCreate()方法中执行的,没有Log打印说明没有执行OnCreate()方法,也就说明是使用的上次的实例,而并不是销毁重建。
经过上面的验证可以得出:在启动一个singleTask的Activity实例时,如果系统中已经存在这样的一个实例,就会将这个实例调到任务栈的栈顶,并清除它当前躲在任务中位于它上面的suoyoudeactivity。
四 实例验证singleInstance启动模式
根据官方文档singleInstance启动模式有以下特点:
1.以singleInstance模式启动的Activity具有全局唯一性,即整个系统中只会存在一个这样的实例
2.以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中(官方文档上的描述为,singleInstance模式的Activity不允 许其他Activity和它共存在一个任务中)
3.被singleInstance模式的Activity开启的其他activity,能够开启一个新任务,但不一定开启新的任务,也可能在已有的一个任务中开启
下面开始验证上面的三个特点,先对之前的项目TestLaunchermode进行修改,清单文件如下:
在上面主要有三个activity,MainActivity启动SecondActivity,SecondActivity启动ThirdActivity,其中SecondActivity的启动方式是SingleInstance,为了方便访问,设置intentFilter属性:com.example.testlaunchermode.ACTION_MY。并且对
TestLaunchermode2进行相应的修改,使得它的MainActivity能够启动SecondActivity,主要代码如下:
btn_next=(Button) findViewById(R.id.btn_next);
btn_next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent("com.example.testlaunchermode.ACTION_MY");
startActivity(intent);
}
});
运行TestLaunchermode,点击MainActivity中的按钮启动SecondActivity,将会发现:
输入命令:adb shell dumpsys activity,将会看到:
这说明singleInstance模式的activity总会在一个新的任务中运行(前提是还没有存在一个这样的实例)
为了验证它的全局唯一性,运行TestLaunchermode2,点击MainActivity中的按钮启动SecondActivity。Log输入如下所示:
Log没有输出新的关于SecondActivity的信息说明是重用了已经存在的实例,因此可以得出结论:以singleInstance模式启动的activity在整个系统中都是单例的,如果在启动这样的activity时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。
下面开始验证第二个特点:以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中
重新运行TestLaunchermode,在MainActivity中点击按钮启动SecondActivity,然后点击SecondActivity中的按钮启动ThirdActivity,Log信息如下:
从上面的输出可以得出以下结论:以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中
下面开始验证第三个特点:被singleInstance模式的Activity开启的其他activity,能够开启一个新任务,但不一定开启新的任务,也可能在已有的一个任务中开启
从第二个特点的验证可以看出,启动ThirdActivity并没有在新的任务中启动而是还在在原来的任务中启动,对上面的SecondActivity添加taskAffinity属性如下所示:
此时Log的打印消息为:
这就说明被singleInstance模式的Activity A在开启另一activity B时,能够开启一个新任务,但是是不是真的开启新任务,还要受其他条件的限制,这个条件是:当前系统中是不是已经有了一个activity B的taskAffinity属性指定的任务。
其实这种行为和singleTask启动时的情况相同。在Activity的启动模式设置为singleTask时,启动时系统会为它加上FLAG_ACTIVITY_NEW_TASK标志,而被singleInstance模式的Activity开启的activity,启动时系统也会为它加上FLAG_ACTIVITY_NEW_TASK标志,所以他们启动时的情况是相同的,上面再验证singleTask时已经阐述过,现在重新说明一下:
由于ThirdActivity是被启动模式为singleInstance类型的Activity(即SecondActivity)启动的,framework会为它它加上FLAG_ACTIVITY_NEW_TASK标志,这时 framework会检索是否已经存在了一个affinity为com.example.testlaunchermode.second(即ThirdActivity的taskAffinity属性)的任务
如果存在这样的一个任务,则检查在这个任务中是否已经有了一个ThirdActivity的实例,
如果已经存在一个ThirdActivity的实例,则会重用这个任务和任务中的ThirdActivity实例,将这个任务调到前台,清除位于ThirdActivity上面的所有Activity,显示ThirdActivity,并调用ThirdActivity的 onNewIntent()。
如果不存在一个ThirdActivity的实例,会在这个任务中创建ThirdActivity的实例,并调用onCreate()方法
如果不存在这样的一个任务,会创建一个新的affinity为com.example.testlaunchermode.second的任务,并且将ThirdActivity启动到这个新的任务中
如果ThirdActivity不设置taskAffinity,即ThirdActivity和MainActivity的taskAffinity相同,都为应用的包名,那么ThirdActivity是不会开启一个新任务的,framework中的判定过程如下:
1.在SecondActivity启动ThirdActivity时,因为SecondActivity是singleInstance的,所以设定ThirdActivity的启动标志为FLAG_ACTIVITY_NEW_TASK
2.然后获得ThirdActivity的taskAffinity,即为包名com.example.testlaunchermode.second
3.检查是否已经存在一个affinity为com.example.testlaunchermode的任务,这个任务是存在的,就是MainActivity所在的任务,这个任务是在启动MainActivity时开启的
4.既然已经存在这个任务,就检索在这个任务中是否存在一个ThirdActivity的实例,发现不存在
5.在这个已有的任务中启动一个SecondActivity的实例
为了作一个清楚的比较,列出ThirdActivity的taskAffinity属性设为com.example.testlaunchermode.second时的启动过程
1.在SecondActivity启动ThirdActivity时,因为SecondActivity是singleInstance的,那么设定ThirdActivity的启动标志为FLAG_ACTIVITY_NEW_TASK
2.然后获得ThirdActivity的taskAffinity,即为com.example.testlaunchermode.second
3.检查是否已经存在一个affinity为com.example.testlaunchermode.second的任务,这个任务是不存在的
4.创建一个新的affinity为com.example.testlaunchermode.second的任务,并且将ThirdActivity启动到这个新的任务
到此singleInstance也介绍完了。
五 本文总结
由上述可知,Task是Android Framework中的一个概念,Task是由一系列相关的Activity组成的,是一组相关Activity的集合。Task是以栈的形式来管理的。
我们在操作软件的过程中,一定会涉及界面的跳转。其实在对界面进行跳转时,Android Framework既能在同一个任务中对Activity进行调度,也能以Task为单位进行整体调度。在启动模式为standard或singleTop时,一般是在同一个任务中对Activity进行调度,而在启动模式为singleTask或singleInstance是,一般会对Task进行整体调度。对Task进行整体调度包括以下操作:
1.按Home键,将之前的任务切换到后台
2.长按Home键,会显示出最近执行过的任务列表
3.在Launcher或HomeScreen点击app图标,开启一个新任务,或者是将已有的任务调度到前台
4.启动singleTask模式的Activity时,会在系统中搜寻是否已经存在一个合适的任务,若存在,则会将这个任务调度到前台以重用这个任务。如果这个任务中已经存在一个要启动的Activity的实例,则清除这个实例之上的所有Activity,将这个实例显示给用户。如果这个已存在的任务中不存在一个要启动的Activity的实例,则在这个任务的顶端启动一个实例。若这个任务不存在,则会启动一个新的任务,在这个新的任务中启动这个singleTask模式的Activity的一个实例。
5.启动singleInstance的Activity时,会在系统中搜寻是否已经存在一个这个Activity的实例,如果存在,会将这个实例所在的任务调度到前台,重用这个Activity的实例(该任务中只有这一个Activity),如果不存在,会开启一个新任务,并在这个新任务中启动这个singleInstance模式的Activity的一个实例。