在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的启动模式的使用之前,首先对这些概念做一个简单的说明和区分。
- <?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>
- <activity android:name='.MyActivity' android:label='@string/app_nam'
- android:process=':remote'>
- </activity>
- <activity android:name='.app.InterstitialMessageActivity'
- android:label='@string/interstitial_label'
- android:theme='@style/Theme.Dialog'
- android:launchMode='singleTask'
- </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实例。
- <?xml version='1.0' encoding='utf-8'?>
- <manifest xmlns:android='http://schemas.android.com/apk/res/android'
- package='com.jg.zhang.androidtasktest'
- android:versionCode='1'
- android:versionName='1.0' >
- <uses-sdk android:minSdkVersion='10' android:targetSdkVersion='17' />
- <application android:icon='@drawable/ic_launcher' android:label='@string/app_name'>
- <activity android:label='@string/app_name'
- android:name='com.jg.zhang.androidtasktest.MainActivity'>
- <intent-filter>
- <action android:name='android.intent.action.MAIN' />
- <category android:name='android.intent.category.LAUNCHER' />
- </intent-filter>
- </activity>
- <!--android:taskAffinity='com.jg.zhang.androidtasktest.second'
- android:alwaysRetainTaskState='true'
- android:allowBackup='true' -->
- <activity android:name='com.jg.zhang.androidtasktest.SecondActivity'
- android:launchMode='singleTask'>
- <intent-filter >
- <action android:name='com.jg.zhang.androidtasktest.SecondActivity'/>
- <category android:name='android.intent.category.DEFAULT'/>
- </intent-filter>
- </activity>
- <activity android:name='com.jg.zhang.androidtasktest.ThirdActivity'
- android:label='@string/app_name' >
- </activity>
- </application>
- </manifest>
- public class MainActivity extends Activity {
- private static final String ACTIVITY_NAME = 'MainActivity';
- private static final String LOG_TAG = 'xxxx';
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(MainActivity.this, SecondActivity.class);
- startActivity(intent);
- }
- });
- int taskId = getTaskId();
- Log.i(LOG_TAG, ACTIVITY_NAME +'所在的任务的id为: ' + taskId);
- }
- public class SecondActivity extends Activity {
- private static final String ACTIVITY_NAME = 'SecondActivity';
- private static final String LOG_TAG = 'xxxx';
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_second);
- findViewById(R.id.button2).setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);
- startActivity(intent);
- }
- });
- int taskId = getTaskId();
- Log.i(LOG_TAG, ACTIVITY_NAME +'所在的任务的id为: ' + taskId);
- }
- public class ThirdActivity extends Activity {
- private static final String ACTIVITY_NAME = 'ThirdActivity';
- private static final String LOG_TAG = 'xxxx';
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_third);
- int taskId = getTaskId();
- Log.i(LOG_TAG, ACTIVITY_NAME +'所在的任务的id为: ' + taskId);
- }
TaskRecord{412ded08 #8 A com.jg.zhang.androidtasktest}
Run #2: ActivityRecord{412c91e8 com.jg.zhang.androidtasktest/.SecondActivity}Run #1: ActivityRecord{412c08a0 com.jg.zhang.androidtasktest/.MainActivity}
- <activity android:name='com.jg.zhang.androidtasktest.SecondActivity'
- android:launchMode='singleTask'
- android:taskAffinity='com.jg.zhang.androidtasktest.second'>
- <intent-filter >
- <action android:name='com.jg.zhang.androidtasktest.SecondActivity'/>
- <category android:name='android.intent.category.DEFAULT'/>
- </intent-filter>
- </activity>
TaskRecord{411e6a88 #6 A com.jg.zhang.androidtasktest.second}
Run #3: ActivityRecord{411c8ea0 com.jg.zhang.androidtasktest/.ThirdActivity}Run #2: ActivityRecord{412bc870 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412ece18 #5 A com.jg.zhang.androidtasktest}
Run #1: ActivityRecord{412924c0 com.jg.zhang.androidtasktest/.MainActivity}
这就可以解释上面示例中的现象了,由第5条可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity为com.jg.zhang.androidtasktest,SecondActivity的taskAffinity为com.jg.zhang.androidtasktest.second,根据上面第4条,taskAffinity可以影响当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。这句话的意思是,当新启动的activity(SecondActivity)是以FLAG_ACTIVITY_NEW_TASK标志启动时(可以认为FLAG_ACTIVITY_NEW_TASK和singleTask作用相同,当启动模式为singleTask时,framework会将它的启动标志设为FLAG_ACTIVITY_NEW_TASK),framework会检索是否已经存在了一个affinity为com.jg.zhang.androidtasktest.second的任务(即一个TaskRecord对象)
上面讨论的是设置taskAffinity属性的情况,如果SecondActivity只设置启动模式为singleTask,而不设置taskAffinity,即三个Activity的taskAffinity相同,都为应用的包名,那么SecondActivity是不会开启一个新任务的,framework中的判定过程如下:
为了作一个清楚的比较,列出SecondActivity启动模式设为singleTask,并且taskAffinity设为com.jg.zhang.androidtasktest.second时的启动过程
其实framework中对任务和activity‘的调度是很复杂的,尤其是把启动模式设为singleTask或者以FLAG_ACTIVITY_NEW_TASK标志启动时。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK时,要仔细测试应用程序。这也是官方文档上的建议。
- <?xml version='1.0' encoding='utf-8'?>
- <manifest xmlns:android='http://schemas.android.com/apk/res/android'
- package='com.jg.zhang.androidtasktest1'
- android:versionCode='1' android:versionName='1.0' >
- <uses-sdk android:minSdkVersion='9' android:targetSdkVersion='17' />
- <application
- android:allowBackup='true' android:icon='@drawable/ic_launcher' android:label='@string/app_name'
- android:theme='@style/AppTheme' >
- <activity
- android:name='com.jg.zhang.androidtasktest1.MainActivity'
- android:label='com.jg.zhang.androidtasktest1.MainActivity' >
- <intent-filter>
- <action android:name='android.intent.action.MAIN' />
- <category android:name='android.intent.category.LAUNCHER' />
- </intent-filter>
- </activity>
- <activity
- android:name='com.jg.zhang.androidtasktest1.OtherActivity'
- android:label='com.jg.zhang.androidtasktest1.OtherActivity'
- android:taskAffinity='com.jg.zhang.androidtasktest.second'
- android:launchMode='singleTask'>
- </activity>
- </application>
- </manifest>
TaskRecord{412370c0 #4 A com.jg.zhang.androidtasktest.second}Intent { cmp=com.jg.zhang.androidtasktest/.SecondActivity }
Hist #4: ActivityRecord{412f5ba0 com.jg.zhang.androidtasktest1/.OtherActivity}
Intent { flg=0x400000 cmp=com.jg.zhang.androidtasktest1/.OtherActivity }
ProcessRecord{412adb28 479:com.jg.zhang.androidtasktest1/10044}
Hist #3: ActivityRecord{4125c880 com.jg.zhang.androidtasktest/.SecondActivity}
Intent { cmp=com.jg.zhang.androidtasktest/.SecondActivity }ProcessRecord{41218e48 463:com.jg.zhang.androidtasktest/10043}
TaskRecord{412f0f60 #5 A com.jg.zhang.androidtasktest1}Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest1/.MainActivity }
Hist #2: ActivityRecord{413045a8 com.jg.zhang.androidtasktest1/.MainActivity}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest1/.MainActivity }ProcessRecord{412adb28 479:com.jg.zhang.androidtasktest1/10044}
TaskRecord{412c5928 #3 A com.jg.zhang.androidtasktest}Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest/.MainActivity }
Hist #0: ActivityRecord{41250850 com.jg.zhang.androidtasktest/.MainActivity}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest/.MainActivity }
ProcessRecord{41218e48 463:com.jg.zhang.androidtasktest/10043}
- <?xml version='1.0' encoding='utf-8'?>
- <manifest xmlns:android='http://schemas.android.com/apk/res/android'
- package='com.jg.zhang.androidtasktest'
- android:versionCode='1'
- android:versionName='1.0' >
- <uses-sdk android:minSdkVersion='10' android:targetSdkVersion='17' />
- <application android:allowBackup='true'
- android:icon='@drawable/ic_launcher' android:label='androidtasktest'>
- <activity android:name='com.jg.zhang.androidtasktest.MainActivity'>
- <intent-filter>
- <action android:name='android.intent.action.MAIN' />
- <category android:name='android.intent.category.LAUNCHER' />
- </intent-filter>
- </activity>
- <activity android:name='com.jg.zhang.androidtasktest.SecondActivity'
- android:launchMode='singleTask'/>
- <activity android:name='com.jg.zhang.androidtasktest.ThirdActivity'/>
- <activity android:name='com.jg.zhang.androidtasktest.FourthActivity'/>
- </application>
- </manifest>
TaskRecord{412e9458 #6 A com.jg.zhang.androidtasktest}
Run #4: ActivityRecord{412e12e8 com.jg.zhang.androidtasktest/.FourthActivity}
Run #3: ActivityRecord{412a9e30 com.jg.zhang.androidtasktest/.ThirdActivity}
Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}
Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}
TaskRecord{412e9458 #6 A com.jg.zhang.androidtasktest}
Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}
Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}
- <?xml version='1.0' encoding='utf-8'?>
- <manifest xmlns:android='http://schemas.android.com/apk/res/android'
- package='com.jg.zhang.androidtasktest'
- android:versionCode='1'
- android:versionName='1.0' >
- <uses-sdk android:minSdkVersion='10' android:targetSdkVersion='17' />
- <application android:allowBackup='true'
- android:icon='@drawable/ic_launcher' android:label='androidtasktest'>
- <activity android:name='com.jg.zhang.androidtasktest.MainActivity'>
- <intent-filter>
- <action android:name='android.intent.action.MAIN' />
- <category android:name='android.intent.category.LAUNCHER' />
- </intent-filter>
- </activity>
- <activity android:name='com.jg.zhang.androidtasktest.SecondActivity'
- android:launchMode='singleInstance'>
- <intent-filter>
- <action android:name='com.jg.zhang.androidtasktest.ACTION_MY'/>
- <category android:name='android.intent.category.DEFAULT'/>
- </intent-filter>
- </activity>
- <activity android:name='com.jg.zhang.androidtasktest.ThirdActivity'/>
- </application>
- </manifest>
由上面的清单文件可以知道,该应用包括三个activity,分别为MianActivity,SecondActivity,ThirdActivity,其中SecondActivity启动模式设置为singleInstance。MianActivity可以开启SecondActivity,SecondActivity可以开启ThirdActivity。 并且为了可以在其他应用中开启SecondActivity,为SecondActivity设置了一个IntentFilter,这样就可以在其他应用中使用隐式Intent开启SecondActivity。
为了更好的验证singleInstance的全局唯一性,还需要其他一个应用,对上面的AndroidTaskTest1进行一些修改即可。AndroidTaskTest1只需要一个MianActivity,在MainActivity中点击按钮会开启AndroidTaskTest应用中的SecondActivity。开启AndroidTaskTest应用中的SecondActivity的代码如下:
- /**
- * 该方法在布局中按钮的android:onClick属性中指定
- * android:onClick='launchOtherActivity'
- * @param v
- */
- public void launchOtherActivity(View v){
- Intent intent = new Intent();
- //以下Action为'com.jg.zhang.androidtasktest.ACTION_MY'
- //即AndroidTaskTest应用中SecondActivity的action
- intent.setAction('com.jg.zhang.androidtasktest.ACTION_MY');
- startActivity(intent);
- }
执行adb shell dumpsys activity命令,有以下输出:
TaskRecord{411189e0 #9 A com.jg.zhang.androidtasktest}
Run #2: ActivityRecord{4129af80 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{41305528 #8 A com.jg.zhang.androidtasktest}
Run #1: ActivityRecord{41296e60 com.jg.zhang.androidtasktest/.MainActivity}
以上可以说明,singleInstance模式的Activity总是会在新的任务中运行(前提是系统中还不存在这样的一个实例) 。
下面验证它的全局唯一性,执行以下操作:安装另一个应用AndroidTaskTest1,在开启的MainActivity中点击按钮开启AndroidTaskTest应用中的SecondActivity。看到打印出一条新的日志:
执行adb shell dumpsys activity命令,有以下输出:
TaskRecord{411189e0 #9 A com.jg.zhang.androidtasktest}
Run #3: ActivityRecord{4129af80 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412dc788 #12 A com.jg.zhang.androidtasktest1}
Run #2: ActivityRecord{4121c628 com.jg.zhang.androidtasktest1/.MainActivity}
TaskRecord{41305528 #8 A com.jg.zhang.androidtasktest}
Run #1: ActivityRecord{41296e60 com.jg.zhang.androidtasktest/.MainActivity}
由红色字体可以得知,开启的SecondActivity就是上次创建的编号为4129af80的SecondActivity,并且Log中没有再次输出关于SecondActivity的信息,说明SecondActivity并没有重新创建。由此可以得出结论:以singleInstance模式启动的Activity在整个系统中是单例的,如果在启动这样的Activiyt时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。
TaskRecord{412a95b8 #15 A com.jg.zhang.androidtasktest}
Run #3: ActivityRecord{411f9318 com.jg.zhang.androidtasktest/.ThirdActivity}
TaskRecord{41353a68 #16 A com.jg.zhang.androidtasktest}
Run #2: ActivityRecord{413537c8 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412a95b8 #15 A com.jg.zhang.androidtasktest}
Run #1: ActivityRecord{4123a0c8 com.jg.zhang.androidtasktest/.MainActivity}
- <activity android:name='com.jg.zhang.androidtasktest.ThirdActivity'
- android:taskAffinity='com.jg.zhang.androidtasktest.second'/>
TaskRecord{413551b0 #20 A com.jg.zhang.androidtasktest.second}
Run #3: ActivityRecord{412de9c0 com.jg.zhang.androidtasktest/.ThirdActivity}
TaskRecord{4134b268 #19 A com.jg.zhang.androidtasktest}
Run #2: ActivityRecord{412a36a0 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{413131e8 #18 A com.jg.zhang.androidtasktest}
Run #1: ActivityRecord{41271e10 com.jg.zhang.androidtasktest/.MainActivity}
由于ThirdActivity是被启动模式为singleInstance类型的Activity(即SecondActivity)启动的,framework会为它它加上FLAG_ACTIVITY_NEW_TASK标志,这时 framework会检索是否已经存在了一个affinity为com.jg.zhang.androidtasktest.second(即ThirdActivity的taskAffinity属性)的任务,
如果ThirdActivity不设置taskAffinity,即ThirdActivity和MainActivity的taskAffinity相同,都为应用的包名,那么ThirdActivity是不会开启一个新任务的,framework中的判定过程如下:
为了作一个清楚的比较,列出ThirdActivity的taskAffinity属性设为com.jg.zhang.androidtasktest.second时的启动过程
我们在操作软件的过程中,一定会涉及界面的跳转。其实在对界面进行跳转时,Android Framework既能在同一个任务中对Activity进行调度,也能以Task为单位进行整体调度。在启动模式为standard或singleTop时,一般是在同一个任务中对Activity进行调度,而在启动模式为singleTask或singleInstance是,一般会对Task进行整体调度。
对Task进行整体调度包括以下操作:
联系客服