在android應用開發(fā)中,打造良好的用戶體驗是非常重要的。而在用戶體驗中,界面的引導和跳轉(zhuǎn)是值得深入研究的重要內(nèi)容。在開發(fā)中,與界面跳轉(zhuǎn)聯(lián)系比較緊密的概念是Task(任務)和Back Stack(回退棧)。activity的啟動模式會影響Task和Back Stack的狀態(tài),進而影響用戶體驗。除了啟動模式之外,Intent類中定義的一些標志(以FLAG_ACTIVITY_開頭)也會影響Task和Back Stack的狀態(tài)。在這篇文章中主要對四種啟動模式進行分析和驗證,其中涉及到activity的一個重要屬性taskAffinity和Intent中的標志之一FLAG_ACTIVITY_NEW_TASK。關于Intent中其他標志位的具體用法會在另一篇文章中介紹。 Task是一個存在于Framework層的概念,容易與它混淆的有Application(應用)和Process(進程)。在開始介紹Activity的啟動模式的使用之前,首先對這些概念做一個簡單的說明和區(qū)分。 一 Application,Task和Process的區(qū)別與聯(lián)系
application翻譯成中文時一般稱為“應用”或“應用程序”,在android中,總體來說一個應用就是一組組件的集合。眾所周知,android是在應用層組件化程度非常高的系統(tǒng),android開發(fā)的第一課就是學習android的四大組件。當我們寫完了多個組件,并且在manifest文件中注冊了這些組件之后,把這些組件和組件使用到的資源打包成apk,我們就可以說完成了一個application。application和組件的關系可以在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是一組相互關聯(lián)的activity的集合,它是存在于framework層的一個概念,控制界面的跳轉(zhuǎn)和返回。這個task存在于一個稱為back stack的數(shù)據(jù)結(jié)構(gòu)中,也就是說,framework是以棧的形式管理用戶開啟的activity。這個棧的基本行為是,當用戶在多個activity之間跳轉(zhuǎn)時,執(zhí)行壓棧操作,當用戶按返回鍵時,執(zhí)行出棧操作。舉例來說,如果應用程序中存在A,B,C三個activity,當用戶在Launcher或Home Screen點擊應用程序圖標時,啟動主Activity A,接著A開啟B,B開啟C,這時棧中有三個Activity,并且這三個Activity默認在同一個任務(task)中,當用戶按返回時,彈出C,棧中只剩A和B,再按返回鍵,彈出B,棧中只剩A,再繼續(xù)按返回鍵,彈出A,任務被移除。如下圖所示:
task是可以跨應用的,這正是task存在的一個重要原因。有的Activity,雖然不在同一個app中,但為了保持用戶操作的連貫性,把他們放在同一個任務中。例如,在我們的應用中的一個Activity A中點擊發(fā)送郵件,會啟動郵件程序的一個Activity B來發(fā)送郵件,這兩個activity是存在于不同app中的,但是被系統(tǒng)放在一個任務中,這樣當發(fā)送完郵件后,用戶按back鍵返回,可以返回到原來的Activity A中,這樣就確保了用戶體驗。
說完了application和task,最后介紹process。process一般翻譯成進程,進程是操作系統(tǒng)內(nèi)核中的一個概念,表示直接受內(nèi)核調(diào)度的執(zhí)行單位。在應用程序的角度看,我們用java編寫的應用程序,運行在dalvik虛擬機中,可以認為一個運行中的dalvik虛擬機實例占有一個進程,所以,在默認情況下,一個應用程序的所有組件運行在同一個進程中。但是這種情況也有例外,即,應用程序中的不同組件可以運行在不同的進程中。只需要在manifest中用process屬性指定組件所運行的進程的名字。如下所示:
<activity android:name='.MyActivity' android:label='@string/app_nam' android:process=':remote'>
這樣的話這個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'
同樣,在Intent類中定義了很多與Activity啟動或調(diào)度有關的標志,<activity>標簽中有一些屬性,這些標志,屬性和四種啟動模式聯(lián)合使用,會在很大程度上改變activity的行為,進而會改變task和back stask的狀態(tài)。關于Intent中的標志和<activity>標簽中有一些屬性會在本文后面介紹,在這一節(jié)中,先介紹activity的四種啟動模式。
standard標準啟動模式,也是activity的默認啟動模式。在這種模式下啟動的activity可以被多次實例化,即在同一個任務中可以存在多個activity的實例,每個實例都會處理一個Intent對象。如果Activity A的啟動模式為standard,并且A已經(jīng)啟動,在A中再次啟動Activity A,即調(diào)用startActivity(new Intent(this,A.class)),會在A的上面再次啟動一個A的實例,即當前的桟中的狀態(tài)為A-->A。
singleTop如果一個以singleTop模式啟動的activity的實例已經(jīng)存在于任務桟的桟頂,那么再啟動這個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()方法。這是任務桟中還是這有一個A的實例。
如果以singleTop模式啟動的activity的一個實例已經(jīng)存在與任務桟中,但是不在桟頂,那么它的行為和standard模式相同,也會創(chuàng)建多個實例。
singleTask谷歌的官方文檔上稱,如果一個activity的啟動模式為singleTask,那么系統(tǒng)總會在一個新任務的最底部(root)啟動這個activity,并且被這個activity啟動的其他activity會和該activity同時存在于這個新任務中。如果系統(tǒng)中已經(jīng)存在這樣的一個activity則會重用這個實例,并且調(diào)用他的onNewIntent()方法。即,這樣的一個activity在系統(tǒng)中只會存在一個實例。
singleInstance總是在新的任務中開啟,并且這個新的任務中有且只有這一個實例,也就是說被該實例啟動的其他activity會自動運行于另一個任務中。當再次啟動該activity的實例時,會重用已存在的任務和實例。并且會調(diào)用這個實例的onNewIntent()方法,將Intent實例傳遞到該實例中。和singleTask相同,同一時刻在系統(tǒng)中只會存在一個這樣的Activity實例。
三 實例驗證singleTask啟動模式
上面將activity的四種啟動模式就基本介紹完了。為了加深對啟動模式的了解,下面會通過一個簡單的例子進行驗證。由以上的介紹可知,standard和singleTop這兩種啟動模式行為比較簡單,所以在下面的例子中,會對singleTask和singleInstance著重介紹。
驗證啟動singleTask模式的activity時是否會創(chuàng)建新的任務
以下為驗證示例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都是標準的啟動模式,而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 +'所在的任務的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 +'所在的任務的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 +'所在的任務的id為: ' + taskId);
以上三個activity只列出了onCreate()方法中的內(nèi)容,實現(xiàn)的邏輯為在MainActivity中點擊按鈕啟動SecondActivity,在SecondActivity中點擊按鈕啟動ThirdActivity。并且在onCreate方法中會以log的形式打印出當前activity所屬的任務(Task)的Id。 現(xiàn)在執(zhí)行以下操作,運行該示例,并且點擊MainActivity界面中的按鈕,開啟SecondActivity。在該示例中SecondActivity的啟動模式為singleTask。按照官方文檔的說法,SecondActivity會在一個新的任務中開啟。但是查看打印出的log,發(fā)現(xiàn)MainActivity和SecondActivity所在的任務的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是啟動在同一個任務中的。其實,把啟動模式設置為singleTask,framework在啟動該activity時只會把它標示為可在一個新任務中啟動,至于是否在一個新任務中啟動,還要受其他條件的限制。現(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。在官方文檔中可以得到關于taskAffinity的以下信息 - taskAffinity表示當前activity具有親和力的一個任務(翻譯不是很準確,原句為The task that the activity has an affinity for.),大致可以這樣理解,這個 taskAffinity表示一個任務,這個任務就是當前activity所在的任務。
- 在概念上,具有相同的affinity的activity(即設置了相同taskAffinity屬性的activity)屬于同一個任務。
- 一個任務的affinity決定于這個任務的根activity(root activity)的taskAffinity。
- 這個屬性決定兩件事:當activity被re-parent時,它可以被re-paren哪個任務中;當activity以FLAG_ACTIVITY_NEW_TASK標志啟動時,它會被啟動到哪個任務中。(這個比較 難以理解,請結(jié)合<activity>中的屬性allowTaskReparenting和Intent中的標志 FLAG_ACTIVITY_NEW_TASK加以理解)
- 默認情況下,一個應用中的所有activity具有相同的taskAffinity,即應用程序的包名。我們可以通過設置不同的taskAffinity屬性給應用中的activity分組,也可以把不同的 應用中的activity的taskAffinity設置成相同的值。
- 為一個activity的taskAffinity設置一個空字符串,表明這個activity不屬于任何task。
這就可以解釋上面示例中的現(xiàn)象了,由第5條可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity為com.jg.zhang.androidtasktest,SecondActivity的taskAffinity為com.jg.zhang.androidtasktest.second,根據(jù)上面第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會檢索是否已經(jīng)存在了一個affinity為com.jg.zhang.androidtasktest.second的任務(即一個TaskRecord對象) - 如果存在這樣的一個任務,則檢查在這個任務中是否已經(jīng)有了一個SecondActivity的實例,
- 如果已經(jīng)存在一個SecondActivity的實例,則會重用這個任務和任務中的SecondActivity實例,將這個任務調(diào)到前臺,清除位于SecondActivity上面的所有Activity,顯示SecondActivity,并調(diào)用SecondActivity的onNewIntent();
- 如果不存在一個SecondActivity的實例,會在這個任務中創(chuàng)建SecondActivity的實例,并調(diào)用onCreate()方法
- 如果不存在這樣的一個任務,會創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務,并且將SecondActivity啟動到這個新的任務中
上面討論的是設置taskAffinity屬性的情況,如果SecondActivity只設置啟動模式為singleTask,而不設置taskAffinity,即三個Activity的taskAffinity相同,都為應用的包名,那么SecondActivity是不會開啟一個新任務的,framework中的判定過程如下: - 在MainActivity啟動SecondActivity時,發(fā)現(xiàn)啟動模式為singleTask,那么設定他的啟動標志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得SecondActivity的taskAffinity,即為包名com.jg.zhang.androidtasktest
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest的任務,這個任務是存在的,就是MainActivity所在的任務,這個任務是在啟動MainActivity時開啟的
- 既然已經(jīng)存在這個任務,就檢索在這個任務中是否存在一個SecondActivity的實例,發(fā)現(xiàn)不存在
- 在這個已有的任務中啟動一個SecondActivity的實例
為了作一個清楚的比較,列出SecondActivity啟動模式設為singleTask,并且taskAffinity設為com.jg.zhang.androidtasktest.second時的啟動過程 - 在MainActivity啟動SecondActivity時,發(fā)現(xiàn)啟動模式為singleTask,那么設定他的啟動標志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得SecondActivity的taskAffinity,即com.jg.zhang.androidtasktest.second
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest.second的任務,這個任務是不存在的
- 創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務,并且將SecondActivity啟動到這個新的任務中
其實framework中對任務和activity‘的調(diào)度是很復雜的,尤其是把啟動模式設為singleTask或者以FLAG_ACTIVITY_NEW_TASK標志啟動時。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK時,要仔細測試應用程序。這也是官方文檔上的建議。 實例驗證將兩個不同app中的不同的singleTask模式的Activity的taskAffinity設成相同
官方文檔中提到,可以把不同的 應用中的activity的taskAffinity設置成相同的值,這樣的話這兩個activity雖然不在同一應用中,卻會在運行時分配到同一任務中,下面對此進行驗證,在這里,會使用上面的示例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的啟動模式被設置為singleTask,并且taskAffinity屬性被設置為com.jg.zhang.androidtasktest.second,這和AndroidTaskTest應用中的SecondActivity相同?,F(xiàn)在將這兩個應用安裝在設備上。執(zhí)行以下操作:
啟動AndroidTaskTest應用,在它的MianActivity中點擊按鈕開啟SecondActivity,由上面的介紹可知secondActivity是運行在一個新任務中的,這個任務就是com.jg.zhang.androidtasktest.second。
然后按Home鍵回到Launcher,啟動AndroidTaskTest1,在啟動AndroidTaskTest1的入口Activity(MianActivity)時,會自動啟動新的任務,那么現(xiàn)在一共有三個任務,AndroidTaskTest的MianActivity和SecondActivity分別占用一個任務,AndroidTaskTest1的MianActivity也占用一個任務。
在AndroidTaskTest1的MianActivity中點擊按鈕啟動OtherActivity,那么這個OtherActivity是在哪個任務中呢?
下面執(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是在同一任務中的。由上面adb shell dumpsys activity命令的輸出結(jié)果(藍色字體)還可以看出,AndroidTaskTest和AndroidTaskTest1這兩個應用程序會開啟兩個進程,他們的所有組件分別運行在獨立的進程中,其中AndroidTaskTest所在進程的進程號為10043,AndroidTaskTest1所在進程的進程號為10044。com.jg.zhang.androidtasktest.second任務中的兩個activity屬于不同的應用,并且運行在不同的進程中,這也說明了一個問題:任務(Task)不僅可以跨應用(Application),還可以跨進程(Process)。
實例驗證singleTask的另一意義:在同一個任務中具有唯一性
谷歌官方文檔中提到,singleTask模式的activity總會在一個新的任務中開啟。上面已經(jīng)驗證了這種說法不確切,singleTask模式只意味著“可以在一個新的任務中開啟”,至于是不是真的會在新任務中開啟,在framework中還有其他條件的限制。由上面的介紹可知,這個條件為:是否已經(jīng)存在了一個由他的taskAffinity屬性指定的任務。這一點具有迷惑性,我們在看到singleTask這個單詞的時候,會直觀的想到它的本意:single in task。即,在同一個任務中,只會有一個該activity的實例?,F(xiàn)在讓我們進行驗證:
為了驗證這種情況,需要修改一下上面用到的AndroidTaskTest示例。增加一個FourthActivity,并且MianActivity,SecondActivity,ThirdActivity和FourthActivity這四個activity都不設置taskAffinity屬性,并且將SecondActivity啟動模式設為singleTask,這樣這四個activity會在同一個任務中開啟。他們的開啟流程是這樣的: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都是在同一個任務中的。再次執(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的任務中,即棧中的狀態(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。確實確保了在任務中是唯一的,并且清除了同一任務中它上面的所有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)度到任務棧的棧頂,并清除它當前所在任務中位于它上面的所有的activity。
四 實例驗證singleInstance的行為
根據(jù)上面的講解,并且參考谷歌官方文檔,singleInstance的特點可以歸結(jié)為以下三條: - 以singleInstance模式啟動的Activity具有全局唯一性,即整個系統(tǒng)中只會存在一個這樣的實例
- 以singleInstance模式啟動的Activity具有獨占性,即它會獨自占用一個任務,被他開啟的任何activity都會運行在其他任務中(官方文檔上的描述為,singleInstance模式的Activity不允許其他Activity和它共存在一個任務中)
- 被singleInstance模式的Activity開啟的其他activity,能夠開啟一個新任務,但不一定開啟新的任務,也可能在已有的一個任務中開啟
下面對這三個特點分別驗證,所使用的示例同樣為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'/>
由上面的清單文件可以知道,該應用包括三個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' 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');
下面開始驗證第一個特點:以singleInstance模式啟動的Activity具有全局唯一性,即整個系統(tǒng)中只會存在一個這樣的實例
執(zhí)行如下操作:安裝AndroidTaskTest應用,點擊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總是會在新的任務中運行(前提是系統(tǒng)中還不存在這樣的一個實例) 。 下面驗證它的全局唯一性,執(zhí)行以下操作:安裝另一個應用AndroidTaskTest1,在開啟的MainActivity中點擊按鈕開啟AndroidTaskTest應用中的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中沒有再次輸出關于SecondActivity的信息,說明SecondActivity并沒有重新創(chuàng)建。由此可以得出結(jié)論:以singleInstance模式啟動的Activity在整個系統(tǒng)中是單例的,如果在啟動這樣的Activiyt時,已經(jīng)存在了一個實例,那么會把它所在的任務調(diào)度到前臺,重用這個實例。 下面開始驗證第二個特點:以singleInstance模式啟動的Activity具有獨占性,即它會獨自占用一個任務,被他開啟的任何activity都會運行在其他任務中
重新安裝AndroidTaskTest應用,點擊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所在的任務為16,被SecondActivity啟動的ThirdActivity所在的任務為15,這就說明以singleInstance模式啟動的Activity具有獨占性,即它會獨自占用一個任務,被他開啟的任何activity都會運行在其他任務中
下面開始驗證第三個特點:被singleInstance模式的Activity開啟的其他activity,能夠在新的任務中啟動,但不一定開啟新的任務,也可能在已有的一個任務中開啟
有上面對第二個特點的驗證可以看到,被SecondActivity啟動的ThirdActivity并沒有運行在一個新開啟的任務中,而是和MainActivity運行在了同一個已有的任務中,那么在什么情況下ThirdActivity才會啟動一個新的任務呢?
現(xiàn)在對程序的清單文件做以下修改,為ThirdActivity增加一個屬性taskAffinity:
<activity android:name='com.jg.zhang.androidtasktest.ThirdActivity' android:taskAffinity='com.jg.zhang.androidtasktest.second'/>
重新安裝AndroidTaskTest應用,執(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啟動在了一個新的任務中,即在啟動ThirdActivity時創(chuàng)建了一個新任務。這就說明被singleInstance模式的Activity A在開啟另一activity B時,能夠開啟一個新任務,但是是不是真的開啟新任務,還要受其他條件的限制,這個條件是:當前系統(tǒng)中是不是已經(jīng)有了一個activity B的taskAffinity屬性指定的任務。
其實這種行為和singleTask啟動時的情況相同。在Activity的啟動模式設置為singleTask時,啟動時系統(tǒng)會為它加上FLAG_ACTIVITY_NEW_TASK標志,而被singleInstance模式的Activity開啟的activity,啟動時系統(tǒng)也會為它加上FLAG_ACTIVITY_NEW_TASK標志,所以他們啟動時的情況是相同的,上面再驗證singleTask時已經(jīng)闡述過,現(xiàn)在重新說明一下:
由于ThirdActivity是被啟動模式為singleInstance類型的Activity(即SecondActivity)啟動的,framework會為它它加上FLAG_ACTIVITY_NEW_TASK標志,這時 framework會檢索是否已經(jīng)存在了一個affinity為com.jg.zhang.androidtasktest.second(即ThirdActivity的taskAffinity屬性)的任務, - 如果存在這樣的一個任務,則檢查在這個任務中是否已經(jīng)有了一個ThirdActivity的實例,
- 如果已經(jīng)存在一個ThirdActivity的實例,則會重用這個任務和任務中的ThirdActivity實例,將這個任務調(diào)到前臺,清除位于ThirdActivity上面的所有Activity,顯示ThirdActivity,并調(diào)用ThirdActivity的onNewIntent()。
- 如果不存在一個ThirdActivity的實例,會在這個任務中創(chuàng)建ThirdActivity的實例,并調(diào)用onCreate()方法
- 如果不存在這樣的一個任務,會創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務,并且將ThirdActivity啟動到這個新的任務中
如果ThirdActivity不設置taskAffinity,即ThirdActivity和MainActivity的taskAffinity相同,都為應用的包名,那么ThirdActivity是不會開啟一個新任務的,framework中的判定過程如下: - 在SecondActivity啟動ThirdActivity時,因為SecondActivity是singleInstance的,所以設定ThirdActivity的啟動標志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得ThirdActivity的taskAffinity,即為包名com.jg.zhang.androidtasktest
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest的任務,這個任務是存在的,就是MainActivity所在的任務,這個任務是在啟動MainActivity時開啟的
- 既然已經(jīng)存在這個任務,就檢索在這個任務中是否存在一個ThirdActivity的實例,發(fā)現(xiàn)不存在
- 在這個已有的任務中啟動一個SecondActivity的實例
為了作一個清楚的比較,列出ThirdActivity的taskAffinity屬性設為com.jg.zhang.androidtasktest.second時的啟動過程 - 在SecondActivity啟動ThirdActivity時,因為SecondActivity是singleInstance的,那么設定ThirdActivity的啟動標志為FLAG_ACTIVITY_NEW_TASK
- 然后獲得ThirdActivity的taskAffinity,即為com.jg.zhang.androidtasktest.second
- 檢查是否已經(jīng)存在一個affinity為com.jg.zhang.androidtasktest.second的任務,這個任務是不存在的
- 創(chuàng)建一個新的affinity為com.jg.zhang.androidtasktest.second的任務,并且將ThirdActivity啟動到這個新的任務
到此singleInstance也介紹完了。
五 本文小結(jié)
由上述可知,Task是Android Framework中的一個概念,Task是由一系列相關的Activity組成的,是一組相關Activity的集合。Task是以棧的形式來管理的。
我們在操作軟件的過程中,一定會涉及界面的跳轉(zhuǎn)。其實在對界面進行跳轉(zhuǎn)時,Android Framework既能在同一個任務中對Activity進行調(diào)度,也能以Task為單位進行整體調(diào)度。在啟動模式為standard或singleTop時,一般是在同一個任務中對Activity進行調(diào)度,而在啟動模式為singleTask或singleInstance是,一般會對Task進行整體調(diào)度。 對Task進行整體調(diào)度包括以下操作: - 按Home鍵,將之前的任務切換到后臺
- 長按Home鍵,會顯示出最近執(zhí)行過的任務列表
- 在Launcher或HomeScreen點擊app圖標,開啟一個新任務,或者是將已有的任務調(diào)度到前臺
- 啟動singleTask模式的Activity時,會在系統(tǒng)中搜尋是否已經(jīng)存在一個合適的任務,若存在,則會將這個任務調(diào)度到前臺以重用這個任務。如果這個任務中已經(jīng)存在一個要啟動的Activity的實例,則清除這個實例之上的所有Activity,將這個實例顯示給用戶。如果這個已存在的任務中不存在一個要啟動的Activity的實例,則在這個任務的頂端啟動一個實例。若這個任務不存在,則會啟動一個新的任務,在這個新的任務中啟動這個singleTask模式的Activity的一個實例。
- 啟動singleInstance的Activity時,會在系統(tǒng)中搜尋是否已經(jīng)存在一個這個Activity的實例,如果存在,會將這個實例所在的任務調(diào)度到前臺,重用這個Activity的實例(該任務中只有這一個Activity),如果不存在,會開啟一個新任務,并在這個新任務中啟動這個singleInstance模式的Activity的一個實例。
|