在android應(yīng)用開發(fā)中,打造良好的用戶體驗是非常重要的。而在用戶體驗中,界面的引導(dǎo)和跳轉(zhuǎn)是值得深入研究的重要內(nèi)容。在開發(fā)中,與界面跳轉(zhuǎn)聯(lián)系比較緊密的概念是Task(任務(wù))和Back Stack(回退棧)。activity的啟動模式會影響Task和Back Stack的狀態(tài),進而影響用戶體驗。除了啟動模式之外,Intent類中定義的一些標(biāo)志(以FLAG_ACTIVITY_開頭)也會影響Task和Back Stack的狀態(tài)。在這篇文章中主要對四種啟動模式進行分析和驗證,其中涉及到activity的一個重要屬性taskAffinity和Intent中的標(biāo)志之一FLAG_ACTIVITY_NEW_TASK。關(guān)于Intent中其他標(biāo)志位的具體用法會在另一篇文章中介紹。 Task是一個存在于Framework層的概念,容易與它混淆的有Application(應(yīng)用)和Process(進程)。在開始介紹Activity的啟動模式的使用之前,首先對這些概念做一個簡單的說明和區(qū)分。 一 Application,Task和Process的區(qū)別與聯(lián)系
application翻譯成中文時一般稱為“應(yīng)用”或“應(yīng)用程序”,在android中,總體來說一個應(yīng)用就是一組組件的集合。眾所周知,android是在應(yīng)用層組件化程度非常高的系統(tǒng),android開發(fā)的第一課就是學(xué)習(xí)android的四大組件。當(dāng)我們寫完了多個組件,并且在manifest文件中注冊了這些組件之后,把這些組件和組件使用到的資源打包成apk,我們就可以說完成了一個application。application和組件的關(guān)系可以在manifest文件中清晰地體現(xiàn)出來。如下所示:
<?xml version='1.0' encoding='utf-8'?> <manifest android:versionCode='1' xmlns:android='http://schemas./apk/res/android' package='com.example.android.myapp'> <application android:label='@string/app_name'> <activity android:name='.MyActivity' android:label='@string/app_nam'> <action android:name='android.intent.action.MAIN' /> <category android:name='android.intent.category.LAUNCHER' /> <receiver android:name='.MyReceiver'/> <provider android:name='.MyProvider'/> <service android:name='.MyService'/>
由此可見,application是由四大組件組成的。在app安裝時,系統(tǒng)會讀取manifest的信息,將所有的組件解析出來,以便在運行時對組件進行實例化和調(diào)度。
而task是在程序運行時,只針對activity的概念。說白了,task是一組相互關(guān)聯(lián)的activity的集合,它是存在于framework層的一個概念,控制界面的跳轉(zhuǎn)和返回。這個task存在于一個稱為back stack的數(shù)據(jù)結(jié)構(gòu)中,也就是說,framework是以棧的形式管理用戶開啟的activity。這個棧的基本行為是,當(dāng)用戶在多個activity之間跳轉(zhuǎn)時,執(zhí)行壓棧操作,當(dāng)用戶按返回鍵時,執(zhí)行出棧操作。舉例來說,如果應(yīng)用程序中存在A,B,C三個activity,當(dāng)用戶在Launcher或Home Screen點擊應(yīng)用程序圖標(biāo)時,啟動主Activity A,接著A開啟B,B開啟C,這時棧中有三個Activity,并且這三個Activity默認(rèn)在同一個任務(wù)(task)中,當(dāng)用戶按返回時,彈出C,棧中只剩A和B,再按返回鍵,彈出B,棧中只剩A,再繼續(xù)按返回鍵,彈出A,任務(wù)被移除。如下圖所示:
task是可以跨應(yīng)用的,這正是task存在的一個重要原因。有的Activity,雖然不在同一個app中,但為了保持用戶操作的連貫性,把他們放在同一個任務(wù)中。例如,在我們的應(yīng)用中的一個Activity A中點擊發(fā)送郵件,會啟動郵件程序的一個Activity B來發(fā)送郵件,這兩個activity是存在于不同app中的,但是被系統(tǒng)放在一個任務(wù)中,這樣當(dāng)發(fā)送完郵件后,用戶按back鍵返回,可以返回到原來的Activity A中,這樣就確保了用戶體驗。
說完了application和task,最后介紹process。process一般翻譯成進程,進程是操作系統(tǒng)內(nèi)核中的一個概念,表示直接受內(nèi)核調(diào)度的執(zhí)行單位。在應(yīng)用程序的角度看,我們用java編寫的應(yīng)用程序,運行在dalvik虛擬機中,可以認(rèn)為一個運行中的dalvik虛擬機實例占有一個進程,所以,在默認(rèn)情況下,一個應(yīng)用程序的所有組件運行在同一個進程中。但是這種情況也有例外,即,應(yīng)用程序中的不同組件可以運行在不同的進程中。只需要在manifest中用process屬性指定組件所運行的進程的名字。如下所示:
<activity android:name='.MyActivity' android:label='@string/app_nam' android:process=':remote'>
這樣的話這個activity會運行在一個獨立的進程中。
二 Activity四種啟動模式詳解
activity有四種啟動模式,分別為standard,singleTop,singleTask,singleInstance。如果要使用這四種啟動模式,必須在manifest文件中<activity>標(biāo)簽中的launchMode屬性中配置,如:
<activity android:name='.app.InterstitialMessageActivity' android:label='@string/interstitial_label' android:theme='@style/Theme.Dialog' android:launchMode='singleTask'
同樣,在Intent類中定義了很多與Activity啟動或調(diào)度有關(guān)的標(biāo)志,<activity>標(biāo)簽中有一些屬性,這些標(biāo)志,屬性和四種啟動模式聯(lián)合使用,會在很大程度上改變activity的行為,進而會改變task和back stask的狀態(tài)。關(guān)于Intent中的標(biāo)志和<activity>標(biāo)簽中有一些屬性會在本文后面介紹,在這一節(jié)中,先介紹activity的四種啟動模式。
standard標(biāo)準(zhǔn)啟動模式,也是activity的默認(rèn)啟動模式。在這種模式下啟動的activity可以被多次實例化,即在同一個任務(wù)中可以存在多個activity的實例,每個實例都會處理一個Intent對象。如果Activity A的啟動模式為standard,并且A已經(jīng)啟動,在A中再次啟動Activity A,即調(diào)用startActivity(new Intent(this,A.class)),會在A的上面再次啟動一個A的實例,即當(dāng)前的桟中的狀態(tài)為A-->A。
singleTop如果一個以singleTop模式啟動的activity的實例已經(jīng)存在于任務(wù)桟的桟頂,那么再啟動這個Activity時,不會創(chuàng)建新的實例,而是重用位于棧頂?shù)哪莻€實例,并且會調(diào)用該實例的onNewIntent()方法將Intent對象傳遞到這個實例中。舉例來說,如果A的啟動模式為singleTop,并且A的一個實例已經(jīng)存在于棧頂中,那么再調(diào)用startActivity(new Intent(this,A.class))啟動A時,不會再次創(chuàng)建A的實例,而是重用原來的實例,并且調(diào)用原來實例的onNewIntent()方法。這是任務(wù)桟中還是這有一個A的實例。
如果以singleTop模式啟動的activity的一個實例已經(jīng)存在與任務(wù)桟中,但是不在桟頂,那么它的行為和standard模式相同,也會創(chuàng)建多個實例。
singleTask谷歌的官方文檔上稱,如果一個activity的啟動模式為singleTask,那么系統(tǒng)總會在一個新任務(wù)的最底部(root)啟動這個activity,并且被這個activity啟動的其他activity會和該activity同時存在于這個新任務(wù)中。如果系統(tǒng)中已經(jīng)存在這樣的一個activity則會重用這個實例,并且調(diào)用他的onNewIntent()方法。即,這樣的一個activity在系統(tǒng)中只會存在一個實例。
singleInstance總是在新的任務(wù)中開啟,并且這個新的任務(wù)中有且只有這一個實例,也就是說被該實例啟動的其他activity會自動運行于另一個任務(wù)中。當(dāng)再次啟動該activity的實例時,會重用已存在的任務(wù)和實例。并且會調(diào)用這個實例的onNewIntent()方法,將Intent實例傳遞到該實例中。和singleTask相同,同一時刻在系統(tǒng)中只會存在一個這樣的Activity實例。
三 實例驗證singleTask啟動模式
上面將activity的四種啟動模式就基本介紹完了。為了加深對啟動模式的了解,下面會通過一個簡單的例子進行驗證。由以上的介紹可知,standard和singleTop這兩種啟動模式行為比較簡單,所以在下面的例子中,會對singleTask和singleInstance著重介紹。
驗證啟動singleTask模式的activity時是否會創(chuàng)建新的任務(wù)
以下為驗證示例AndroidTaskTest。這個實例中有三個Activity,分別為:MainActivity,SecondActivity和ThirdActivity。以下為這個示例的manifest文件。
<?xml version='1.0' encoding='utf-8'?> <manifest xmlns:android='http://schemas./apk/res/android' package='com.jg.zhang.androidtasktest' 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'> <action android:name='android.intent.action.MAIN' /> <category android:name='android.intent.category.LAUNCHER' /> <!--android:taskAffinity='com.jg.zhang.androidtasktest.second' android:alwaysRetainTaskState='true' android:allowBackup='true' --> <activity android:name='com.jg.zhang.androidtasktest.SecondActivity' android:launchMode='singleTask'> <action android:name='com.jg.zhang.androidtasktest.SecondActivity'/> <category android:name='android.intent.category.DEFAULT'/> <activity android:name='com.jg.zhang.androidtasktest.ThirdActivity' android:label='@string/app_name' >
由此可見,MainActivity和ThirdActivity都是標(biāo)準(zhǔn)的啟動模式,而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'; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { Intent intent = new Intent(MainActivity.this, SecondActivity.class); int taskId = getTaskId(); Log.i(LOG_TAG, ACTIVITY_NAME +'所在的任務(wù)的id為: ' + taskId);
SecondActivity
public class SecondActivity extends Activity { private static final String ACTIVITY_NAME = 'SecondActivity'; private static final String LOG_TAG = 'xxxx'; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); findViewById(R.id.button2).setOnClickListener(new OnClickListener() { public void onClick(View v) { Intent intent = new Intent(SecondActivity.this, ThirdActivity.class); int taskId = getTaskId(); Log.i(LOG_TAG, ACTIVITY_NAME +'所在的任務(wù)的id為: ' + taskId);
ThirdActivity public class ThirdActivity extends Activity { private static final String ACTIVITY_NAME = 'ThirdActivity'; private static final String LOG_TAG = 'xxxx'; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_third); int taskId = getTaskId(); Log.i(LOG_TAG, ACTIVITY_NAME +'所在的任務(wù)的id為: ' + taskId);
以上三個activity只列出了onCreate()方法中的內(nèi)容,實現(xiàn)的邏輯為在MainActivity中點擊按鈕啟動SecondActivity,在SecondActivity中點擊按鈕啟動ThirdActivity。并且在onCreate方法中會以log的形式打印出當(dāng)前activity所屬的任務(wù)(Task)的Id。 現(xiàn)在執(zhí)行以下操作,運行該示例,并且點擊MainActivity界面中的按鈕,開啟SecondActivity。在該示例中SecondActivity的啟動模式為singleTask。按照官方文檔的說法,SecondActivity會在一個新的任務(wù)中開啟。但是查看打印出的log,發(fā)現(xiàn)MainActivity和SecondActivity所在的任務(wù)的Id相同。
在命令行中執(zhí)行以下命令 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是啟動在同一個任務(wù)中的。其實,把啟動模式設(shè)置為singleTask,framework在啟動該activity時只會把它標(biāo)示為可在一個新任務(wù)中啟動,至于是否在一個新任務(wù)中啟動,還要受其他條件的限制。現(xiàn)在在SecondActivity增加一個taskAffinity屬性,如下所示:
<activity android:name='com.jg.zhang.androidtasktest.SecondActivity' android:launchMode='singleTask' android:taskAffinity='com.jg.zhang.androidtasktest.second'> <action android:name='com.jg.zhang.androidtasktest.SecondActivity'/> <category android:name='android.intent.category.DEFAULT'/>
重新運行該示例,執(zhí)行相同的操作,即:點擊MainActivity界面中的按鈕,開啟SecondActivity,并且點擊SecondActivity中的按鈕,啟動ThirdActivity,log中輸出的內(nèi)容為:
在命令行中執(zhí)行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}
在這里便引出了manifest文件中<activity>的一個重要屬性,taskAffinity。在官方文檔中可以得到關(guān)于taskAffinity的以下信息 - taskAffinity表示當(dāng)前activity具有親和力的一個任務(wù)(翻譯不是很準(zhǔn)確,原句為The task that the activity has an affinity for.),大致可以這樣理解,這個 taskAffinity表示一個任務(wù),這個任務(wù)就是當(dāng)前activity所在的任務(wù)。
- 在概念上,具有相同的affinity的activity(即設(shè)置了相同taskAffinity屬性的activity)屬于同一個任務(wù)。
- 一個任務(wù)的affinity決定于這個任務(wù)的根activity(root activity)的taskAffinity。
- 這個屬性決定兩件事:當(dāng)activity被re-parent時,它可以被re-paren哪個任務(wù)中;當(dāng)activity以FLAG_ACTIVITY_NEW_TASK標(biāo)志啟動時,它會被啟動到哪個任務(wù)中。(這個比較 難以理解,請結(jié)合<activity>中的屬性allowTaskReparenting和Intent中的標(biāo)志 FLAG_ACTIVITY_NEW_TASK加以理解)
- 默認(rèn)情況下,一個應(yīng)用中的所有activity具有相同的taskAffinity,即應(yīng)用程序的包名。我們可以通過設(shè)置不同的taskAffinity屬性給應(yīng)用中的activity分組,也可以把不同的 應(yīng)用中的activity的taskAffinity設(shè)置成相同的值。
- 為一個activity的taskAffinity設(shè)置一個空字符串,表明這個activity不屬于任何task。
這就可以解釋上面示例中的現(xiàn)象了,由第5條可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity為com.jg.zhang.androidtasktest,SecondActivity的taskAffinity為com.jg.zhang.androidtasktest.second,根據(jù)上面第4條,taskAffinity可以影響當(dāng)activity以FLAG_ACTIVITY_NEW_TASK標(biāo)志啟動時,它會被啟動到哪個任務(wù)中。這句話的意思是,當(dāng)新啟動的activity(SecondActivity)是以FLAG_ACTIVITY_NEW_TASK標(biāo)志啟動時(可以認(rèn)為FLAG_ACTIVITY_NEW_TASK和singleTask作用相同,當(dāng)啟動模式為singleTask時,framework會將它的啟動標(biāo)志設(shè)為FLAG_ACTIVITY_NEW_TASK),framework會檢索是否已經(jīng)存在了一個affinity為com.jg.zhang.androidtasktest.second的任務(wù)(即一個TaskRecord對象) - 如果存在這樣的一個任務(wù),則檢查在這個任務(wù)中是否已經(jīng)有了一個SecondActivity的實例,
- 如果已經(jīng)存在一個SecondActivity的實例,則會重用這個任務(wù)和任務(wù)中的SecondActivity實例,將這個任務(wù)調(diào)到前臺,清除位于SecondActivity上面的所有Activity,顯示SecondActivity,并調(diào)用SecondActivity的onNewIntent();
- 如果不存在一個SecondActivity的實例,會在這個任務(wù)中創(chuàng)建SecondActivity的實例,并調(diào)用onCreate()方法
- 如果不存在這樣的一個任務(wù),會創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務(wù),并且將SecondActivity啟動到這個新的任務(wù)中
上面討論的是設(shè)置taskAffinity屬性的情況,如果SecondActivity只設(shè)置啟動模式為singleTask,而不設(shè)置taskAffinity,即三個Activity的taskAffinity相同,都為應(yīng)用的包名,那么SecondActivity是不會開啟一個新任務(wù)的,framework中的判定過程如下: - 在MainActivity啟動SecondActivity時,發(fā)現(xiàn)啟動模式為singleTask,那么設(shè)定他的啟動標(biāo)志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得SecondActivity的taskAffinity,即為包名com.jg.zhang.androidtasktest
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest的任務(wù),這個任務(wù)是存在的,就是MainActivity所在的任務(wù),這個任務(wù)是在啟動MainActivity時開啟的
- 既然已經(jīng)存在這個任務(wù),就檢索在這個任務(wù)中是否存在一個SecondActivity的實例,發(fā)現(xiàn)不存在
- 在這個已有的任務(wù)中啟動一個SecondActivity的實例
為了作一個清楚的比較,列出SecondActivity啟動模式設(shè)為singleTask,并且taskAffinity設(shè)為com.jg.zhang.androidtasktest.second時的啟動過程 - 在MainActivity啟動SecondActivity時,發(fā)現(xiàn)啟動模式為singleTask,那么設(shè)定他的啟動標(biāo)志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得SecondActivity的taskAffinity,即com.jg.zhang.androidtasktest.second
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest.second的任務(wù),這個任務(wù)是不存在的
- 創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務(wù),并且將SecondActivity啟動到這個新的任務(wù)中
其實framework中對任務(wù)和activity‘的調(diào)度是很復(fù)雜的,尤其是把啟動模式設(shè)為singleTask或者以FLAG_ACTIVITY_NEW_TASK標(biāo)志啟動時。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK時,要仔細(xì)測試應(yīng)用程序。這也是官方文檔上的建議。 實例驗證將兩個不同app中的不同的singleTask模式的Activity的taskAffinity設(shè)成相同
官方文檔中提到,可以把不同的 應(yīng)用中的activity的taskAffinity設(shè)置成相同的值,這樣的話這兩個activity雖然不在同一應(yīng)用中,卻會在運行時分配到同一任務(wù)中,下面對此進行驗證,在這里,會使用上面的示例AndroidTaskTest,并創(chuàng)建一個新的示例AndroidTaskTest1。AndroidTaskTest1由兩個activity組成,分別為MianActivity和OtherActivity,在MianActivity中點擊按鈕會啟動OtherActivity,該程序的界面和上一個類似,代碼也類似,再此僅列出清單文件。
<?xml version='1.0' encoding='utf-8'?> <manifest xmlns:android='http://schemas./apk/res/android' package='com.jg.zhang.androidtasktest1' android:versionCode='1' android:versionName='1.0' > <uses-sdk android:minSdkVersion='9' android:targetSdkVersion='17' /> android:allowBackup='true' android:icon='@drawable/ic_launcher' android:label='@string/app_name' android:theme='@style/AppTheme' > android:name='com.jg.zhang.androidtasktest1.MainActivity' android:label='com.jg.zhang.androidtasktest1.MainActivity' > <action android:name='android.intent.action.MAIN' /> <category android:name='android.intent.category.LAUNCHER' /> android:name='com.jg.zhang.androidtasktest1.OtherActivity' android:label='com.jg.zhang.androidtasktest1.OtherActivity' android:taskAffinity='com.jg.zhang.androidtasktest.second' android:launchMode='singleTask'>
可以看到OtherActivity的啟動模式被設(shè)置為singleTask,并且taskAffinity屬性被設(shè)置為com.jg.zhang.androidtasktest.second,這和AndroidTaskTest應(yīng)用中的SecondActivity相同?,F(xiàn)在將這兩個應(yīng)用安裝在設(shè)備上。執(zhí)行以下操作:
啟動AndroidTaskTest應(yīng)用,在它的MianActivity中點擊按鈕開啟SecondActivity,由上面的介紹可知secondActivity是運行在一個新任務(wù)中的,這個任務(wù)就是com.jg.zhang.androidtasktest.second。
然后按Home鍵回到Launcher,啟動AndroidTaskTest1,在啟動AndroidTaskTest1的入口Activity(MianActivity)時,會自動啟動新的任務(wù),那么現(xiàn)在一共有三個任務(wù),AndroidTaskTest的MianActivity和SecondActivity分別占用一個任務(wù),AndroidTaskTest1的MianActivity也占用一個任務(wù)。
在AndroidTaskTest1的MianActivity中點擊按鈕啟動OtherActivity,那么這個OtherActivity是在哪個任務(wù)中呢?
下面執(zhí)行adb shell dumpsys activity命令,發(fā)現(xiàn)有以下輸出:
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}
在執(zhí)行上述操作時,打印出的Log為:
所以由此可見,AndroidTaskTest的SecondActivity和AndroidTaskTest1的OtherActivity是在同一任務(wù)中的。由上面adb shell dumpsys activity命令的輸出結(jié)果(藍色字體)還可以看出,AndroidTaskTest和AndroidTaskTest1這兩個應(yīng)用程序會開啟兩個進程,他們的所有組件分別運行在獨立的進程中,其中AndroidTaskTest所在進程的進程號為10043,AndroidTaskTest1所在進程的進程號為10044。com.jg.zhang.androidtasktest.second任務(wù)中的兩個activity屬于不同的應(yīng)用,并且運行在不同的進程中,這也說明了一個問題:任務(wù)(Task)不僅可以跨應(yīng)用(Application),還可以跨進程(Process)。
實例驗證singleTask的另一意義:在同一個任務(wù)中具有唯一性
谷歌官方文檔中提到,singleTask模式的activity總會在一個新的任務(wù)中開啟。上面已經(jīng)驗證了這種說法不確切,singleTask模式只意味著“可以在一個新的任務(wù)中開啟”,至于是不是真的會在新任務(wù)中開啟,在framework中還有其他條件的限制。由上面的介紹可知,這個條件為:是否已經(jīng)存在了一個由他的taskAffinity屬性指定的任務(wù)。這一點具有迷惑性,我們在看到singleTask這個單詞的時候,會直觀的想到它的本意:single in task。即,在同一個任務(wù)中,只會有一個該activity的實例?,F(xiàn)在讓我們進行驗證:
為了驗證這種情況,需要修改一下上面用到的AndroidTaskTest示例。增加一個FourthActivity,并且MianActivity,SecondActivity,ThirdActivity和FourthActivity這四個activity都不設(shè)置taskAffinity屬性,并且將SecondActivity啟動模式設(shè)為singleTask,這樣這四個activity會在同一個任務(wù)中開啟。他們的開啟流程是這樣的:MianActivity開啟SecondActivity,SecondActivity開啟ThirdActivity,ThirdActivity開啟FourthActivity,F(xiàn)ourthActivity開啟SecondActivity。代碼和軟件界面就不列出了,只列出清單文件
<?xml version='1.0' encoding='utf-8'?> <manifest xmlns:android='http://schemas./apk/res/android' package='com.jg.zhang.androidtasktest' 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'> <action android:name='android.intent.action.MAIN' /> <category android:name='android.intent.category.LAUNCHER' /> <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'/>
現(xiàn)在從MianActivity一直啟動到FourthActivity,打印出的系統(tǒng)Log為:
由此可見這四個activity都是在同一個任務(wù)中的。再次執(zhí)行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的任務(wù)中,即棧中的狀態(tài)為MainActivity --> SecondActivity --> ThirdActivity --> FourthActivity。
下面執(zhí)行在FourthActivity中點擊按鈕啟動SecondActivity的操作,注意,SecondActivity的啟動模式為singleTask,那么現(xiàn)在棧中的情況如何呢?再次執(zhí)行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}
這時棧中的狀態(tài)為MainActivity --> SecondActivity。確實確保了在任務(wù)中是唯一的,并且清除了同一任務(wù)中它上面的所有Activity。 那么這個SecondActivity的實例是重用的上次已有的實例還是重新啟動了一個實例呢?可以觀察系統(tǒng)Log, 發(fā)現(xiàn)系統(tǒng)Log沒有改變,還是上面的四條Log。打印Log的語句是在各個Activity中的onCreate方法中執(zhí)行的,沒有打印出新的Log,說明SecondActivity的onCreate的方法沒有重新執(zhí)行,也就是說是重用的上次已經(jīng)啟動的實例,而不是銷毀重建。
經(jīng)過上面的驗證,可以得出如下的結(jié)論:在啟動一個singleTask的Activity實例時,如果系統(tǒng)中已經(jīng)存在這樣一個實例,就會將這個實例調(diào)度到任務(wù)棧的棧頂,并清除它當(dāng)前所在任務(wù)中位于它上面的所有的activity。
四 實例驗證singleInstance的行為
根據(jù)上面的講解,并且參考谷歌官方文檔,singleInstance的特點可以歸結(jié)為以下三條: - 以singleInstance模式啟動的Activity具有全局唯一性,即整個系統(tǒng)中只會存在一個這樣的實例
- 以singleInstance模式啟動的Activity具有獨占性,即它會獨自占用一個任務(wù),被他開啟的任何activity都會運行在其他任務(wù)中(官方文檔上的描述為,singleInstance模式的Activity不允許其他Activity和它共存在一個任務(wù)中)
- 被singleInstance模式的Activity開啟的其他activity,能夠開啟一個新任務(wù),但不一定開啟新的任務(wù),也可能在已有的一個任務(wù)中開啟
下面對這三個特點分別驗證,所使用的示例同樣為AndroidTaskTest,只不過會進行一些修改,下面列出它的清單文件:
<?xml version='1.0' encoding='utf-8'?> <manifest xmlns:android='http://schemas./apk/res/android' package='com.jg.zhang.androidtasktest' 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'> <action android:name='android.intent.action.MAIN' /> <category android:name='android.intent.category.LAUNCHER' /> <activity android:name='com.jg.zhang.androidtasktest.SecondActivity' android:launchMode='singleInstance'> <action android:name='com.jg.zhang.androidtasktest.ACTION_MY'/> <category android:name='android.intent.category.DEFAULT'/> <activity android:name='com.jg.zhang.androidtasktest.ThirdActivity'/>
由上面的清單文件可以知道,該應(yīng)用包括三個activity,分別為MianActivity,SecondActivity,ThirdActivity,其中SecondActivity啟動模式設(shè)置為singleInstance。MianActivity可以開啟SecondActivity,SecondActivity可以開啟ThirdActivity。 并且為了可以在其他應(yīng)用中開啟SecondActivity,為SecondActivity設(shè)置了一個IntentFilter,這樣就可以在其他應(yīng)用中使用隱式Intent開啟SecondActivity。 為了更好的驗證singleInstance的全局唯一性,還需要其他一個應(yīng)用,對上面的AndroidTaskTest1進行一些修改即可。AndroidTaskTest1只需要一個MianActivity,在MainActivity中點擊按鈕會開啟AndroidTaskTest應(yīng)用中的SecondActivity。開啟AndroidTaskTest應(yīng)用中的SecondActivity的代碼如下: * 該方法在布局中按鈕的android:onClick屬性中指定 * android:onClick='launchOtherActivity' public void launchOtherActivity(View v){ Intent intent = new Intent(); //以下Action為'com.jg.zhang.androidtasktest.ACTION_MY' //即AndroidTaskTest應(yīng)用中SecondActivity的action intent.setAction('com.jg.zhang.androidtasktest.ACTION_MY');
下面開始驗證第一個特點:以singleInstance模式啟動的Activity具有全局唯一性,即整個系統(tǒng)中只會存在一個這樣的實例
執(zhí)行如下操作:安裝AndroidTaskTest應(yīng)用,點擊MainActivity中的按鈕,開啟SecondActivity,可以看到如下log輸出: 
執(zhí)行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總是會在新的任務(wù)中運行(前提是系統(tǒng)中還不存在這樣的一個實例) 。 下面驗證它的全局唯一性,執(zhí)行以下操作:安裝另一個應(yīng)用AndroidTaskTest1,在開啟的MainActivity中點擊按鈕開啟AndroidTaskTest應(yīng)用中的SecondActivity??吹酱蛴〕鲆粭l新的日志: 
執(zhí)行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就是上次創(chuàng)建的編號為4129af80的SecondActivity,并且Log中沒有再次輸出關(guān)于SecondActivity的信息,說明SecondActivity并沒有重新創(chuàng)建。由此可以得出結(jié)論:以singleInstance模式啟動的Activity在整個系統(tǒng)中是單例的,如果在啟動這樣的Activiyt時,已經(jīng)存在了一個實例,那么會把它所在的任務(wù)調(diào)度到前臺,重用這個實例。 下面開始驗證第二個特點:以singleInstance模式啟動的Activity具有獨占性,即它會獨自占用一個任務(wù),被他開啟的任何activity都會運行在其他任務(wù)中
重新安裝AndroidTaskTest應(yīng)用,點擊MainActivity中的按鈕,開啟SecondActivity,在SecondActivity中點擊按鈕,開啟ThirdActivity??梢钥吹接腥缦翷og輸出:
執(zhí)行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所在的任務(wù)為16,被SecondActivity啟動的ThirdActivity所在的任務(wù)為15,這就說明以singleInstance模式啟動的Activity具有獨占性,即它會獨自占用一個任務(wù),被他開啟的任何activity都會運行在其他任務(wù)中
下面開始驗證第三個特點:被singleInstance模式的Activity開啟的其他activity,能夠在新的任務(wù)中啟動,但不一定開啟新的任務(wù),也可能在已有的一個任務(wù)中開啟
有上面對第二個特點的驗證可以看到,被SecondActivity啟動的ThirdActivity并沒有運行在一個新開啟的任務(wù)中,而是和MainActivity運行在了同一個已有的任務(wù)中,那么在什么情況下ThirdActivity才會啟動一個新的任務(wù)呢?
現(xiàn)在對程序的清單文件做以下修改,為ThirdActivity增加一個屬性taskAffinity:
<activity android:name='com.jg.zhang.androidtasktest.ThirdActivity' android:taskAffinity='com.jg.zhang.androidtasktest.second'/>
重新安裝AndroidTaskTest應(yīng)用,執(zhí)行和上一步中同樣的操作:點擊MainActivity中的按鈕,開啟SecondActivity,在SecondActivity中點擊按鈕,開啟ThirdActivity。可以看到有如下輸出:
執(zhí)行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啟動在了一個新的任務(wù)中,即在啟動ThirdActivity時創(chuàng)建了一個新任務(wù)。這就說明被singleInstance模式的Activity A在開啟另一activity B時,能夠開啟一個新任務(wù),但是是不是真的開啟新任務(wù),還要受其他條件的限制,這個條件是:當(dāng)前系統(tǒng)中是不是已經(jīng)有了一個activity B的taskAffinity屬性指定的任務(wù)。
其實這種行為和singleTask啟動時的情況相同。在Activity的啟動模式設(shè)置為singleTask時,啟動時系統(tǒng)會為它加上FLAG_ACTIVITY_NEW_TASK標(biāo)志,而被singleInstance模式的Activity開啟的activity,啟動時系統(tǒng)也會為它加上FLAG_ACTIVITY_NEW_TASK標(biāo)志,所以他們啟動時的情況是相同的,上面再驗證singleTask時已經(jīng)闡述過,現(xiàn)在重新說明一下:
由于ThirdActivity是被啟動模式為singleInstance類型的Activity(即SecondActivity)啟動的,framework會為它它加上FLAG_ACTIVITY_NEW_TASK標(biāo)志,這時 framework會檢索是否已經(jīng)存在了一個affinity為com.jg.zhang.androidtasktest.second(即ThirdActivity的taskAffinity屬性)的任務(wù), - 如果存在這樣的一個任務(wù),則檢查在這個任務(wù)中是否已經(jīng)有了一個ThirdActivity的實例,
- 如果已經(jīng)存在一個ThirdActivity的實例,則會重用這個任務(wù)和任務(wù)中的ThirdActivity實例,將這個任務(wù)調(diào)到前臺,清除位于ThirdActivity上面的所有Activity,顯示ThirdActivity,并調(diào)用ThirdActivity的onNewIntent()。
- 如果不存在一個ThirdActivity的實例,會在這個任務(wù)中創(chuàng)建ThirdActivity的實例,并調(diào)用onCreate()方法
- 如果不存在這樣的一個任務(wù),會創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務(wù),并且將ThirdActivity啟動到這個新的任務(wù)中
如果ThirdActivity不設(shè)置taskAffinity,即ThirdActivity和MainActivity的taskAffinity相同,都為應(yīng)用的包名,那么ThirdActivity是不會開啟一個新任務(wù)的,framework中的判定過程如下: - 在SecondActivity啟動ThirdActivity時,因為SecondActivity是singleInstance的,所以設(shè)定ThirdActivity的啟動標(biāo)志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得ThirdActivity的taskAffinity,即為包名com.jg.zhang.androidtasktest
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest的任務(wù),這個任務(wù)是存在的,就是MainActivity所在的任務(wù),這個任務(wù)是在啟動MainActivity時開啟的
- 既然已經(jīng)存在這個任務(wù),就檢索在這個任務(wù)中是否存在一個ThirdActivity的實例,發(fā)現(xiàn)不存在
- 在這個已有的任務(wù)中啟動一個SecondActivity的實例
為了作一個清楚的比較,列出ThirdActivity的taskAffinity屬性設(shè)為com.jg.zhang.androidtasktest.second時的啟動過程 - 在SecondActivity啟動ThirdActivity時,因為SecondActivity是singleInstance的,那么設(shè)定ThirdActivity的啟動標(biāo)志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得ThirdActivity的taskAffinity,即為com.jg.zhang.androidtasktest.second
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest.second的任務(wù),這個任務(wù)是不存在的
- 創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務(wù),并且將ThirdActivity啟動到這個新的任務(wù)
到此singleInstance也介紹完了。
五 本文小結(jié)
由上述可知,Task是Android Framework中的一個概念,Task是由一系列相關(guān)的Activity組成的,是一組相關(guān)Activity的集合。Task是以棧的形式來管理的。
我們在操作軟件的過程中,一定會涉及界面的跳轉(zhuǎn)。其實在對界面進行跳轉(zhuǎn)時,Android Framework既能在同一個任務(wù)中對Activity進行調(diào)度,也能以Task為單位進行整體調(diào)度。在啟動模式為standard或singleTop時,一般是在同一個任務(wù)中對Activity進行調(diào)度,而在啟動模式為singleTask或singleInstance是,一般會對Task進行整體調(diào)度。 對Task進行整體調(diào)度包括以下操作: - 按Home鍵,將之前的任務(wù)切換到后臺
- 長按Home鍵,會顯示出最近執(zhí)行過的任務(wù)列表
- 在Launcher或HomeScreen點擊app圖標(biāo),開啟一個新任務(wù),或者是將已有的任務(wù)調(diào)度到前臺
- 啟動singleTask模式的Activity時,會在系統(tǒng)中搜尋是否已經(jīng)存在一個合適的任務(wù),若存在,則會將這個任務(wù)調(diào)度到前臺以重用這個任務(wù)。如果這個任務(wù)中已經(jīng)存在一個要啟動的Activity的實例,則清除這個實例之上的所有Activity,將這個實例顯示給用戶。如果這個已存在的任務(wù)中不存在一個要啟動的Activity的實例,則在這個任務(wù)的頂端啟動一個實例。若這個任務(wù)不存在,則會啟動一個新的任務(wù),在這個新的任務(wù)中啟動這個singleTask模式的Activity的一個實例。
- 啟動singleInstance的Activity時,會在系統(tǒng)中搜尋是否已經(jīng)存在一個這個Activity的實例,如果存在,會將這個實例所在的任務(wù)調(diào)度到前臺,重用這個Activity的實例(該任務(wù)中只有這一個Activity),如果不存在,會開啟一個新任務(wù),并在這個新任務(wù)中啟動這個singleInstance模式的Activity的一個實例。
|