1. 程式人生 > >Android中Activity四種啟動模式和taskAffinity屬性詳解

Android中Activity四種啟動模式和taskAffinity屬性詳解

在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檔案中清晰地體現出來。如下所示:
<?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的資訊,將所有的元件解析出來,以便在執行時對元件進行例項化和排程。 而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 android:name=".MyActivity" android:label="@string/app_nam"
		android:process=":remote">
 </activity>
這樣的話這個activity會執行在一個獨立的程序中。

二 Activity四種啟動模式詳解

activity有四種啟動模式,分別為standard,singleTop,singleTask,singleInstance。如果要使用這四種啟動模式,必須在manifest檔案中<activity>標籤中的launchMode屬性中配置,如:
        <activity android:name=".app.InterstitialMessageActivity"
                  android:label="@string/interstitial_label"
                  android:theme="@style/Theme.Dialog"
                  android:launchMode="singleTask"
        </activity>
同樣,在Intent類中定義了很多與Activity啟動或排程有關的標誌,<activity>標籤中有一些屬性,這些標誌,屬性和四種啟動模式聯合使用,會在很大程度上改變activity的行為,進而會改變task和back stask的狀態。關於Intent中的標誌和<activity>標籤中有一些屬性會在本文後面介紹,在這一節中,先介紹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的例項。
如果以singleTop模式啟動的activity的一個例項已經存在與任務桟中,但是不在桟頂,那麼它的行為和standard模式相同,也會建立多個例項。

singleTask

谷歌的官方文件上稱,如果一個activity的啟動模式為singleTask,那麼系統總會在一個新任務的最底部(root)啟動這個activity,並且被這個activity啟動的其他activity會和該activity同時存在於這個新任務中。如果系統中已經存在這樣的一個activity則會重用這個例項,並且呼叫他的onNewIntent()方法。即,這樣的一個activity在系統中只會存在一個例項。 其實官方文件中的這種說法並不準確,啟動模式為singleTask的activity並不會總是開啟一個新的任務。詳情請參考 解開Android應用程式元件Activity的"singleTask"之謎,在本文後面也會通過示例來進行驗證。

singleInstance

總是在新的任務中開啟,並且這個新的任務中有且只有這一個例項,也就是說被該例項啟動的其他activity會自動運行於另一個任務中。當再次啟動該activity的例項時,會重用已存在的任務和例項。並且會呼叫這個例項的onNewIntent()方法,將Intent例項傳遞到該例項中。和singleTask相同,同一時刻在系統中只會存在一個這樣的Activity例項。

三 例項驗證singleTask啟動模式

上面將activity的四種啟動模式就基本介紹完了。為了加深對啟動模式的瞭解,下面會通過一個簡單的例子進行驗證。由以上的介紹可知,standard和singleTop這兩種啟動模式行為比較簡單,所以在下面的例子中,會對singleTask和singleInstance著重介紹。

驗證啟動singleTask模式的activity時是否會建立新的任務

以下為驗證示例AndroidTaskTest。這個例項中有三個Activity,分別為:MainActivity,SecondActivity和ThirdActivity。以下為這個示例的manifest檔案。
<?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。 以下為這三個Activity的介面,很簡單,在MainActivity中點選按鈕啟動SecondActivity,在SecondActivity中點選按鈕啟動ThirdActivity。
以下為這三個activity的主要程式碼: MainActivity
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);
	}


SecondActivity
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。
現在執行以下操作,執行該示例,並且點選MainActivity介面中的按鈕,開啟SecondActivity。在該示例中SecondActivity的啟動模式為singleTask。按照官方文件的說法,SecondActivity會在一個新的任務中開啟。但是檢視打印出的log,發現MainActivity和SecondActivity所在的任務的Id相同。
在命令列中執行以下命令 adb shell dumpsys activity , 有以下輸出:
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}
所以,和官方文件表述的不同,MainActivity和SecondActivity是啟動在同一個任務中的。其實,把啟動模式設定為singleTask,framework在啟動該activity時只會把它標示為可在一個新任務中啟動,至於是否在一個新任務中啟動,還要受其他條件的限制。現在在SecondActivity增加一個taskAffinity屬性,如下所示:
         <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中輸出的內容為:
在命令列中執行adb shell dumpsys 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}
由此可見,MainActivity和SecondActivity執行在不同的任務中了,並且被SecondActivity啟動的ThirdActivity和SecondActivity執行在同一個任務中。這種現象的具體解釋可以參考解開Android應用程式元件Activity的"singleTask"之謎。 在這裡便引出了manifest檔案中<activity>的一個重要屬性,taskAffinity。在官方文件中可以得到關於taskAffinity的以下資訊
  1. taskAffinity表示當前activity具有親和力的一個任務(翻譯不是很準確,原句為The task that the activity has an affinity for.),大致可以這樣理解,這個 taskAffinity表示一個任務,這個任務就是當前activity所在的任務。
  2. 在概念上,具有相同的affinity的activity(即設定了相同taskAffinity屬性的activity)屬於同一個任務。
  3.  一個任務的affinity決定於這個任務的根activity(root activity)的taskAffinity。
  4.  這個屬性決定兩件事:當activity被re-parent時,它可以被re-paren哪個任務中;當activity以FLAG_ACTIVITY_NEW_TASK標誌啟動時,它會被啟動到哪個任務中。(這個比較    難以理解,請結合<activity>中的屬性allowTaskReparenting和Intent中的標誌       FLAG_ACTIVITY_NEW_TASK加以理解)
  5.  預設情況下,一個應用中的所有activity具有相同的taskAffinity,即應用程式的包名。我們可以通過設定不同的taskAffinity屬性給應用中的activity分組,也可以把不同的       應用中的activity的taskAffinity設定成相同的值。
  6.  為一個activity的taskAffinity設定一個空字串,表明這個activity不屬於任何task。

這就可以解釋上面示例中的現象了,由第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物件)

  • 如果存在這樣的一個任務,則檢查在這個任務中是否已經有了一個SecondActivity的例項,
    • 如果已經存在一個SecondActivity的例項,則會重用這個任務和任務中的SecondActivity例項,將這個任務調到前臺,清除位於SecondActivity上面的所有Activity,顯示SecondActivity,並呼叫SecondActivity的onNewIntent();
    • 如果不存在一個SecondActivity的例項,會在這個任務中建立SecondActivity的例項,並呼叫onCreate()方法
  • 如果不存在這樣的一個任務,會建立一個新的affinity為com.jg.zhang.androidtasktest.second的任務,並且將SecondActivity啟動到這個新的任務中

上面討論的是設定taskAffinity屬性的情況,如果SecondActivity只設置啟動模式為singleTask,而不設定taskAffinity,即三個Activity的taskAffinity相同,都為應用的包名,那麼SecondActivity是不會開啟一個新任務的,framework中的判定過程如下:

  1. 在MainActivity啟動SecondActivity時,發現啟動模式為singleTask,那麼設定他的啟動標誌為FLAG_ACTIVITY_NEW_TASK
  2.  然後獲得SecondActivity的taskAffinity,即為包名com.jg.zhang.androidtasktest
  3. 檢查是否已經存在一個affinity為com.jg.zhang.androidtasktest的任務,這個任務是存在的,就是MainActivity所在的任務,這個任務是在啟動MainActivity時開啟的
  4.  既然已經存在這個任務,就檢索在這個任務中是否存在一個SecondActivity的例項,發現不存在
  5.  在這個已有的任務中啟動一個SecondActivity的例項

為了作一個清楚的比較,列出SecondActivity啟動模式設為singleTask,並且taskAffinity設為com.jg.zhang.androidtasktest.second時的啟動過程

  1. 在MainActivity啟動SecondActivity時,發現啟動模式為singleTask,那麼設定他的啟動標誌為FLAG_ACTIVITY_NEW_TASK
  2. 然後獲得SecondActivity的taskAffinity,即com.jg.zhang.androidtasktest.second
  3. 檢查是否已經存在一個affinity為com.jg.zhang.androidtasktest.second的任務,這個任務是不存在的
  4. 建立一個新的affinity為com.jg.zhang.androidtasktest.second的任務,並且將SecondActivity啟動到這個新的任務中

其實framework中對任務和activity‘的排程是很複雜的,尤其是把啟動模式設為singleTask或者以FLAG_ACTIVITY_NEW_TASK標誌啟動時。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK時,要仔細測試應用程式。這也是官方文件上的建議。

例項驗證將兩個不同app中的不同的singleTask模式的Activity的taskAffinity設成相同

官方文件中提到,可以把不同的 應用中的activity的taskAffinity設定成相同的值,這樣的話這兩個activity雖然不在同一應用中,卻會在執行時分配到同一任務中,下面對此進行驗證,在這裡,會使用上面的示例AndroidTaskTest,並建立一個新的示例AndroidTaskTest1。AndroidTaskTest1由兩個activity組成,分別為MianActivity和OtherActivity,在MianActivity中點選按鈕會啟動OtherActivity,該程式的介面和上一個類似,程式碼也類似,再此僅列出清單檔案。
<?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}
在執行上述操作時,打印出的Log為:
所以由此可見,AndroidTaskTest的SecondActivity和AndroidTaskTest1的OtherActivity是在同一任務中的。由上面adb shell dumpsys activity命令的輸出結果(藍色字型)還可以看出,AndroidTaskTest和AndroidTaskTest1這兩個應用程式會開啟兩個程序,他們的所有元件分別執行在獨立的程序中,其中AndroidTaskTest所在程序的程序號為10043,AndroidTaskTest1所在程序的程序號為10044。com.jg.zhang.androidtasktest.second任務中的兩個activity屬於不同的應用,並且執行在不同的程序中,這也說明了一個問題:任務(Task)不僅可以跨應用(Application),還可以跨程序(Process)

例項驗證singleTask的另一意義:在同一個任務中具有唯一性

谷歌官方文件中提到,singleTask模式的activity總會在一個新的任務中開啟。上面已經驗證了這種說法不確切,singleTask模式只意味著“可以在一個新的任務中開啟”,至於是不是真的會在新任務中開啟,在framework中還有其他條件的限制。由上面的介紹可知,這個條件為:是否已經存在了一個由他的taskAffinity屬性指定的任務。這一點具有迷惑性,我們在看到singleTask這個單詞的時候,會直觀的想到它的本意:single in task。即,在同一個任務中,只會有一個該activity的例項。現在讓我們進行驗證: 為了驗證這種情況,需要修改一下上面用到的AndroidTaskTest示例。增加一個FourthActivity,並且MianActivity,SecondActivity,ThirdActivity和FourthActivity這四個activity都不設定taskAffinity屬性,並且將SecondActivity啟動模式設為singleTask,這樣這四個activity會在同一個任務中開啟。他們的開啟流程是這樣的:MianActivity開啟SecondActivity,SecondActivity開啟ThirdActivity,ThirdActivity開啟FourthActivity,FourthActivity開啟SecondActivity。程式碼和軟體介面就不列出了,只列出清單檔案
<?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為:
由此可見這四個activity都是在同一個任務中的。再次執行adb shell dumpsys activity命令加以驗證:
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}
同樣可以說明目前這四個activity都執行在affinity為com.jg.zhang.androidtasktest的任務中,即棧中的狀態為MainActivity -->  SecondActivity --> ThirdActivity --> FourthActivity。 下面執行在FourthActivity中點選按鈕啟動SecondActivity的操作,注意,SecondActivity的啟動模式為singleTask,那麼現在棧中的情況如何呢?再次執行adb shell dumpsys activity命令,有以下輸出:
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}
這時棧中的狀態為MainActivity -->  SecondActivity。確實確保了在任務中是唯一的,並且清除了同一任務中它上面的所有Activity。 那麼這個SecondActivity的例項是重用的上次已有的例項還是重新啟動了一個例項呢?可以觀察系統Log, 發現系統Log沒有改變,還是上面的四條Log。列印Log的語句是在各個Activity中的onCreate方法中執行的,沒有打印出新的Log,說明SecondActivity的onCreate的方法沒有重新執行,也就是說是重用的上次已經啟動的例項,而不是銷燬重建。 經過上面的驗證,可以得出如下的結論:在啟動一個singleTask的Activity例項時,如果系統中已經存在這樣一個例項,就會將這個例項排程到任務棧的棧頂,並清除它當前所在任務中位於它上面的所有的activity。

四 例項驗證singleInstance的行為

根據上面的講解,並且參考谷歌官方文件,singleInstance的特點可以歸結為以下三條:
  1. 以singleInstance模式啟動的Activity具有全域性唯一性,即整個系統中只會存在一個這樣的例項
  2. 以singleInstance模式啟動的Activity具有獨佔性,即它會獨自佔用一個任務,被他開啟的任何activity都會執行在其他任務中(官方文件上的描述為,singleInstance模式的Activity不允許其他Activity和它共存在一個任務中)
  3. 被singleInstance模式的Activity開啟的其他activity,能夠開啟一個新任務,但不一定開啟新的任務,也可能在已有的一個任務中開啟
下面對這三個特點分別驗證,所使用的示例同樣為AndroidTaskTest,只不過會進行一些修改,下面列出它的清單檔案:
<?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);
	}

下面開始驗證第一個特點:以singleInstance模式啟動的Activity具有全域性唯一性,即整個系統中只會存在一個這樣的例項

執行如下操作:安裝AndroidTaskTest應用,點選MainActivity中的按鈕,開啟SecondActivity,可以看到如下log輸出:

執行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時,已經存在了一個例項,那麼會把它所在的任務排程到前臺,重用這個例項。

下面開始驗證第二個特點:以singleInstance模式啟動的Activity具有獨佔性,即它會獨自佔用一個任務,被他開啟的任何activity都會執行在其他任務中

重新安裝AndroidTaskTest應用,點選MainActivity中的按鈕,開啟SecondActivity,在SecondActivity中點選按鈕,開啟ThirdActivity。可以看到有如下Log輸出:
執行adb shell dumpsys activity命令,有以下輸出:
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}
SecondActivity所在的任務為16,被SecondActivity啟動的ThirdActivity所在的任務為15,這就說明以singleInstance模式啟動的Activity具有獨佔性,即它會獨自佔用一個任務,被他開啟的任何activity都會執行在其他任務中

下面開始驗證第三個特點:被singleInstance模式的Activity開啟的其他activity,能夠在新的任務中啟動,但不一定開啟新的任務,也可能在已有的一個任務中開啟

有上面對第二個特點的驗證可以看到,被SecondActivity啟動的ThirdActivity並沒有執行在一個新開啟的任務中,而是和MainActivity執行在了同一個已有的任務中,那麼在什麼情況下ThirdActivity才會啟動一個新的任務呢? 現在對程式的清單檔案做以下修改,為ThirdActivity增加一個屬性taskAffinity:
         <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"
             android:taskAffinity="com.jg.zhang.androidtasktest.second"/>

重新安裝AndroidTaskTest應用,執行和上一步中同樣的操作:點選MainActivity中的按鈕,開啟SecondActivity,在SecondActivity中點選按鈕,開啟ThirdActivity。可以看到有如下輸出:
執行adb shell dumpsys activity命令,有以下輸出:
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}
可見,被SecondActivity啟動的ThirdActivity啟動在了一個新的任務中,即在啟動ThirdActivity時建立了一個新任務。這就說明被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.jg.zhang.androidtasktest.second(即ThirdActivity的taskAffinity屬性)的任務,

  • 如果存在這樣的一個任務,則檢查在這個任務中是否已經有了一個ThirdActivity的例項,
    1. 如果已經存在一個ThirdActivity的例項,則會重用這個任務和任務中的ThirdActivity例項,將這個任務調到前臺,清除位於ThirdActivity上面的所有Activity,顯示ThirdActivity,並呼叫ThirdActivity的onNewIntent()。
    2. 如果不存在一個ThirdActivity的例項,會在這個任務中建立ThirdActivity的例項,並呼叫onCreate()方法
  • 如果不存在這樣的一個任務,會建立一個新的affinity為com.jg.zhang.androidtasktest.second的任務,並且將ThirdActivity啟動到這個新的任務中

如果ThirdActivity不設定taskAffinity,即ThirdActivity和MainActivity的taskAffinity相同,都為應用的包名,那麼ThirdActivity是不會開啟一個新任務的,framework中的判定過程如下:

  1. 在SecondActivity啟動ThirdActivity時,因為SecondActivity是singleInstance的,所以設定ThirdActivity的啟動標誌為FLAG_ACTIVITY_NEW_TASK
  2. 然後獲得ThirdActivity的taskAffinity,即為包名com.jg.zhang.androidtasktest
  3. 檢查是否已經存在一個affinity為com.jg.zhang.androidtasktest的任務,這個任務是存在的,就是MainActivity所在的任務,這個任務是在啟動MainActivity時開啟的
  4.  既然已經存在這個任務,就檢索在這個任務中是否存在一個ThirdActivity的例項,發現不存在
  5.  在這個已有的任務中啟動一個SecondActivity的例項

為了作一個清楚的比較,列出ThirdActivity的taskAffinity屬性設為com.jg.zhang.androidtasktest.second時的啟動過程

  1. 在SecondActivity啟動ThirdActivity時,因為SecondActivity是singleInstance的,那麼設定ThirdActivity的啟動標誌為FLAG_ACTIVITY_NEW_TASK
  2. 然後獲得ThirdActivity的taskAffinity,即為com.jg.zhang.androidtasktest.second
  3. 檢查是否已經存在一個affinity為com.jg.zhang.androidtasktest.second的任務,這個任務是不存在的
  4.  建立一個新的affinity為com.jg.zhang.androidtasktest.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的一個例項。