http://www.xuebuyuan.com/1624447.html
在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>
由此可见,application是由四大组件组成的。在app安装时,系统会读取manifest的信息,将所有的组件解析出来,以便在运行时对组件进行实例化和调度。
<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" 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>
由此可见,MainActivity和ThirdActivity都是标准的启动模式,而SecondActivity的启动模式为singleTask。
<pre name="code" class="java" style="margin-top: 0px; margin-bottom: 10px;">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); }
ThirdActivity
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); }
以上三个activity只列出了onCreate()方法中的内容,实现的逻辑为在MainActivity中点击按钮启动SecondActivity,在SecondActivity中点击按钮启动ThirdActivity。并且在onCreate方法中会以log的形式打印出当前activity所属的任务(Task)的Id。
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>
重新运行该示例,执行相同的操作,即:点击MainActivity界面中的按钮,开启SecondActivity,并且点击SecondActivity中的按钮,启动ThirdActivity,log中输出的内容为:
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>
可以看到OtherActivity的启动模式被设置为singleTask,并且taskAffinity属性被设置为com.jg.zhang.androidtasktest.second,这和AndroidTaskTest应用中的SecondActivity相同。现在将这两个应用安装在设备上。执行以下操作:启动AndroidTaskTest应用,在它的MianActivity中点击按钮开启SecondActivity,由上面的介绍可知secondActivity是运行在一个新任务中的,这个任务就是com.jg.zhang.androidtasktest.second。然后按Home键回到Launcher,启动AndroidTaskTest1,在启动AndroidTaskTest1的入口Activity(MianActivity)时,会自动启动新的任务,那么现在一共有三个任务,AndroidTaskTest的MianActivity和SecondActivity分别占用一个任务,AndroidTaskTest1的MianActivity也占用一个任务。现在在AndroidTaskTest1的MianActivity中点击按钮启动OtherActivity,那么这个OtherActivity是在哪个任务中呢?下面执行adb
shell dumpsys activity命令,发现有以下输出:
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>
现在从MianActivity一直启动到FourthActivity,打印出的系统Log为:
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"/>
重新安装AndroidTaskTest应用,执行和上一步中同样的操作:点击MainActivity中的按钮,开启SecondActivity,在SecondActivity中点击按钮,开启ThirdActivity。可以看到有如下输出:
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时的启动过程
由上述可知,Task是Android Framework中的一个概念,Task是由一系列相关的Activity组成的,是一组相关Activity的集合。Task是以栈的形式来管理的。
我们在操作软件的过程中,一定会涉及界面的跳转。其实在对界面进行跳转时,Android Framework既能在同一个任务中对Activity进行调度,也能以Task为单位进行整体调度。在启动模式为standard或singleTop时,一般是在同一个任务中对Activity进行调度,而在启动模式为singleTask或singleInstance是,一般会对Task进行整体调度。对Task进行整体调度包括以下操作: