1. 程式人生 > >Activity四種啟動模式 圖文詳解:standard, singleTop, singleTask 以及 singleInstance

Activity四種啟動模式 圖文詳解:standard, singleTop, singleTask 以及 singleInstance

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0520/2897.html

啟動模式

啟動模式是什麼

有這樣的場景:

  • 當我們使用App的時候,呈現出一個Activity,按下返回鍵(不考慮重寫返回鍵事件),常常就回退到上一個開啟的Activity或者退出App。
//重寫返回按鍵事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0
) { // 這裡重寫返回鍵 return true; } return false; }
  • 幾個應用之間,例如我的App要使用拍照功能,我需要呼叫系統的相機App,這分明就是兩個不同的應用程式,分別執行在不同的程序,但是當我呼叫完成相機後,按下返回鍵可以返回我的App
//呼叫相機
private void openCamera() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        uri = PhotoUtil.createImageFile();
        if
(takePictureIntent.resolveActivity(mContext.getPackageManager()) != null) {// 相機被解除安裝時不會崩潰 takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri); startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAMERA); } }
還有例如在我們的App開啟瀏覽器、微博之類的應用,然後跳轉到瀏覽器,使用完成瀏覽器的功能,不斷按下返回鍵,可以回到我們的應用。而不是像點選桌面上瀏覽器圖示啟動瀏覽器那樣,不斷返回鍵,最後回到的是桌面。
  • 生成重複頁面。
    以前遇到過這樣的Bug:
    1. 訊息推送,通知欄彈出Notification,點選Notification跳轉到指定Activity,但是如果我現在頁面就停留在那個指定的Activity,會再次開啟我當前的Activity,這樣返回的時候回退的頁面和當前頁面一樣,感官上就會很奇怪。
    2. 登入的時候,登入成功跳轉到主頁,按下兩次登入按鈕,生成了兩個主頁。一些有啟動延遲的頁面(往往是動畫,網路造成)也會有這樣的情況。

為什麼要研究啟動模式

  1. 有時候我們的App需要生成給其他App呼叫的Activity,例如瀏覽器應用,照相機應用
  2. 解決生成重複頁面等等Bug
  3. 任務棧過深的時候,避免一直按返回鍵也退不回想要的頁面

任務棧

任務棧Task,是一種用來放置Activity例項的容器,他是以棧的形式進行盛放,也就是所謂的先進後出,主要有2個基本操作:壓棧和出棧,其所存放的Activity是不支援重新排序的,只能根據壓棧和出棧操作更改Activity的順序。

啟動一個Application的時候,系統會為它預設建立一個對應的Task,用來放置根Activity。預設啟動Activity會放在同一個Task中,新啟動的Activity會被壓入啟動它的那個Activity的棧中,並且顯示它。當用戶按下回退鍵時,這個Activity就會被彈出棧,按下Home鍵回到桌面,再啟動另一個應用,這時候之前那個Task就被移到後臺,成為後臺任務棧,而剛啟動的那個Task就被調到前臺,成為前臺任務棧,手機頁面顯示的就是前臺任務棧中的棧頂元素。

***********************************應用舉例××××××××××××××××××××

standard  : 標準普通 傳統模式。

singleTop:訊息推送/ 全域性搜尋框。

singleTask : 跨應用呼叫單任務,不要出現重複頁面,同時可以避免標準普通模式的呼叫棧過深。

singleInstance:全域性單一例項。如電話來電頁面。

********************************××××××××××××××××××××

Activity是安卓上最聰明的設計之一,優秀的記憶體管理讓多工完美執行在最流行的作業系統之上。並不是讓Activity在螢幕上啟動就完事了,其啟動方式也是需要關注的。這個話題的內容很多,其中很重要的就是啟動模式(launchMode)。這也是我們這篇部落格要討論的內容。

因為不同的Activity有不同的目的。有些被設計成每傳送一個intent都單獨一個Activity工作,比如郵件客戶端中撰寫郵件的Activity,而有些則被設計成單例的,比如郵件收件箱的Activity。

這就是為什麼指明一個Activity是否需要新建還是使用現有Activity是很有必要的,否則可能導致糟糕的使用者體驗。多虧了安卓的核心工程師,讓launchMode可以幫助你專門應對這種情況。

設定一個launchMode

一般地,我們可以直接在AndroidManifest.xml <activity>標籤的一個屬性中設定launchMode,如下:

  1. <activity
  2. android:name=".SingleTaskActivity"
  3. android:label="singleTask launchMode"
  4. android:launchMode="singleTask">

有4種類型的launchMode,我們一個一個的看。

standard

這是預設的模式。

這種模式下,當Intent傳送的時候,Activity總是被建立一個新的出來單獨工作。想象一下,如果有傳送10個撰寫郵件的Intent,那麼將有10個不同的Activity啟動。

Lollipop之前裝置上的表現

這種Activity將被建立並置於棧頂,和傳送intent的Activity處於同一個任務中。注:一般來講,安卓第三個虛擬鍵所列出的那些就是任務。

standardtopstandard

下面的圖片顯示了向標準啟動模式的Activity分享照片時的情況。雖然分別來自不同的應用,但仍然它會和傳送intent的Activity處於同一個任務中。

注:從圖中可以看出分享圖片的是Gallery應用。

standardgallery2

同時你會看到此時工作管理員是這樣的(有一點怪異)。

gallerystandard

如果我們切換到另外一個應用然後再切回到Gallery,你會發現standard launchMode啟動的Activity仍然在Gallery任務的上面,導致在操作Gallery之前,我們必須首先結束這個額外的Activity。

Lollipop裝置上的表現

如果Activity都是來自同一個應用,其表現和Lollipop之前的裝置一樣,在任務的頂端。

standardstandardl

但是如果intent來自其他應用,將建立一個新的任務,同時新建立的Activity會被作為一個根Activity,如下:

standardgalleryl

注:圖片中的Task#2和Task#3分別表示兩個任務,序號大的比序號小的後啟動。

下面是工作管理員中的樣子:

gallerystandardl1

發生這種情況的原因是Lollipop中任務管理系統做了修改,讓它看起來更合理了。因為它們在不同的任務中,你可以直接切回Gallery,你還可以觸發另一個Intent,建立新的與之前相同的任務。

gallerystandardl2

撰寫郵件的Activity或者釋出社交網路狀態的Activity都是採用這種Activity的例子。如果你希望Activity單獨服務於一個Intent,就可以考慮standard啟動模式。

singleTop

接下來就是singleTop模式。它的表現幾乎和standard模式一模一樣,一個singleTop Activity 的例項可以無限多,唯一的區別是如果在棧頂已經有一個相同型別的Activity例項,Intent不會再建立一個Activity,而是通過onNewIntent()被髮送到現有的Activity。

singletop

在singleTop模式下我們需要同時在onCreate() 和 onNewIntent()中處理髮來的intent,以滿足不同情況。

這種啟動模式的用例之一就是搜尋功能。假設我們建立了一個搜尋框,點選搜尋的時候將導航到一個顯示搜尋結果列表的SearchActivity中,為了更好的使用者體驗,這個搜尋框一般也會被放到SearchActivity中,這樣使用者想要再次搜尋就不需要按返回鍵。

想像一下,如果每次顯示搜尋結果的時候我們都啟動一個新的activity,10次搜尋10個activity,那樣當我們想返回最初的那個activity的時候需要按10次返回。

所以我們應該這樣,如果棧頂已經有一個SearchActivity,我們將Intent傳送給現有的activity,讓它來更新搜尋結果。這樣就只會有一個在棧頂的SearchActivity,只需點一次back就可以回到之前的activity。

不管怎樣,singleTop和它的呼叫者處在一個任務中。如果你想要讓intent傳送給另一個任務中處於棧頂的Activity,是不行的。

而當Intent來自於另外一個應用的時候,新的Activity的啟動方式和standard模式是一致的(pre-Lollipop:處於呼叫者任務的棧頂,Lollipop:會建立一個新的任務)。

singleTask

這種模式和standard以及singleTop有很大不同。singleTask模式的Activity只允許在系統中有一個例項。如果系統中已經有了一個例項,持有這個例項的任務將移動到頂部,同時intent將被通過onNewIntent()傳送。如果沒有,則會建立一個新的Activity並置放在合適的任務中。

在同一個應用中的情況

如果系統中還沒有singleTask的Activity,會新建立一個,並放在同一任務的棧頂。

singleTask1

但是如果已經存在,singleTask Activity上面的所有Activity將以合適的方式自動銷燬,讓我們想要顯示的Activity處於棧頂。同時Intent也會通過onNewIntent()方法傳送到這個singleTask Activity。

singleTaskD

在使用者體驗方面,可能不是很合理,但是它就是這樣設計的...

你可能注意到了 官方文件 中提到的一個問題:

 系統會建立一個新的任務,並將這個Activity例項化為新任務的根部(root)。

但從實驗結果來看,並不是這麼回事。singleTask Activity仍然在任務的Activity棧頂,我們可以從dumpsys activity 命令顯示上看出來:

  1. Task id #239
  2. TaskRecord{428efe30#239 A=com.thecheesefactory.lab.launchmode U=0 sz=2}
  3. Intent
  4. { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]
  5.  flg=0x10000000
  6. cmp=com.thecheesefactory.lab.launchmode/.StandardActivity}
  7. Hist#1: ActivityRecord{429a88d0 u0 com.thecheesefactory.lab.launchmode/.SingleTaskActivity t239}
  8. Intent{ cmp=com.thecheesefactory.lab.launchmode/.SingleTaskActivity}
  9. ProcessRecord{4224313018965:com.thecheesefactory.lab.launchmode/u0a123}
  10. Hist#0: ActivityRecord{425fec98 u0 com.thecheesefactory.lab.launchmode/.StandardActivity t239}
  11. Intent
  12. { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]
  13.  flg=0x10000000
  14. cmp=com.thecheesefactory.lab.launchmode/.StandardActivity}
  15. ProcessRecord{4224313018965:com.thecheesefactory.lab.launchmode/u0a123}

如果你希望singleTask Activity表現的和文件中描述的一致,你需要為singleTask Activity設定taskAffinity屬性。

  1. <activity
  2. android:name=".SingleTaskActivity"
  3. android:label="singleTask launchMode"
  4. android:launchMode="singleTask"
  5. android:taskAffinity="">

這裡是啟動SingleTaskActivity的的結果(使用了taskAffinity之後)。

singleTaskTaskAffinity

screenshot17

是否使用taskAffinity取決於你自己。

和其他應用一起工作的情況

一旦intent是從另外的應用傳送過來,並且系統中也沒有任何Activity的例項,則會建立一個新的任務,並且新的Activity被作為根Activity建立。

singleTaskAnotherApp1

singletaskfromapp2

除非擁有這個singleTask Activity 的應用已經存在,那樣的話,新建的Activity會置於這個任務的上面(而不是新建一個任務)。

singleTaskAnotherApp2

In case that there is an Activity instance existed in any Task, the whole Task would be moved to top and every single Activity placed above the singleTask Activity will be destroyed with lifecycle. If back button is pressed, user has to travel through the Activities in the stack before going back to the caller Task.

假設已經有了一個Activity的例項,不管它是在哪個任務中(包括上面的那種情況,在用於這個Activity的應用中),整個任務將被移到頂端,而singleTask  Activity上面的所有 Activity 都將被銷燬, 使用者需要按back鍵遍歷玩棧中的Activity才能回到呼叫者任務。

singleTaskAnotherApp3

這種模式的應用案例有。郵件客戶端的收件箱或者社交網路的時間軸。這些Activity一般不會設計成擁有多個例項,singleTask可以滿足。但是在使用這種模式的時候必須要明智,因為有些Activity會在使用者不知情的情況下被銷燬。

singleInstance

這個模式非常接近於singleTask,系統中只允許一個Activity的例項存在。區別在於持有這個Activity的任務中只能有一個Activity:即這個單例本身。If another Activity is called from this kind of Activity, a new Task would be automatically created to place that new Activity. Likewise, if singleInstance Activity is called, new Task would be created to place the Activity.

不過結果卻很怪異,從dumpsys提供的資訊來看,似乎系統中有兩個任務但工作管理員中只顯示一個,即最後被移到頂部的那個。導致雖然後臺有一個任務在執行,我們卻無法切換回去,這一點也不科學。

下面是當singleInstance Activity被呼叫的同時棧中已經有一些Activity的情況下所發生的事情:

singleInstance

本來有兩個任務,但是工作管理員中卻只顯示一個任務:

singleInstances

Since this Task could has only one Activity, we couldn't switch back to Task #1 anymore. Only way to do so is to relaunch the application from launcher but it appears that the singleInstance Task would be hidden in the background instead.

因為這個任務只有一個Activity,我們再也無法切回到任務#1了。唯一的辦法是重新在launcher中啟動這個應用。 but之後的沒有翻譯,因為我也不明白作者的意思。

不過這個問題也有解決方案,就像我們在singleTask Acvity中做的,只要為singleInstance Activity設定taskAffinity屬性就可以了。

  1. <activity
  2. android:name=".SingleInstanceActivity"
  3. android:label="singleInstance launchMode"
  4. android:launchMode="singleInstance"
  5. android:taskAffinity="">

現在科學多了。

screenshot18

這種模式很少被使用。實際使用的案例如Launcher的Activity或者100%確定只有一個Activity的應用。總之除非完全有必要,不然我不建議使用這種模式。

Intent Flags

除了在AndroidManifest.xml中直接設定launch mode,我們還可以通過叫做 Intent Flags的東西設定更多的行為,比如:

  1. Intent intent =newIntent(StandardActivity.this,StandardActivity.class);
  2. intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
  3. startActivity(intent);

這段程式碼將會啟動一個singleTop啟動模式的的StandardActivity

三 例項驗證singleTask啟動模式

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

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

以下為驗證示例AndroidTaskTest。這個例項中有三個Activity,分別為:MainActivity,SecondActivity和ThirdActivity。以下為這個示例的manifest檔案。
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3.     package="com.jg.zhang.androidtasktest"
  4.     android:versionCode="1"
  5.     android:versionName="1.0">
  6.     <uses-sdkandroid:minSdkVersion="10"android:targetSdkVersion="17"/>
  7.     <applicationandroid:icon="@drawable/ic_launcher"android:label="@string/app_name">
  8.         <activityandroid:label="@string/app_name"
  9.             android:name="com.jg.zhang.androidtasktest.MainActivity">
  10.             <intent-filter>
  11.                 <actionandroid:name="android.intent.action.MAIN"/>
  12.                 <categoryandroid:name="android.intent.category.LAUNCHER"/>
  13.             </intent-filter>
  14.         </activity>
  15.         <!--android:taskAffinity="com.jg.zhang.androidtasktest.second"
  16.             android:alwaysRetainTaskState="true"
  17.             android:allowBackup="true" -->
  18.          <activityandroid:name="com.jg.zhang.androidtasktest.SecondActivity"
  19.              android:launchMode="singleTask">
  20.             <intent-filter>
  21.                 <actionandroid:name="com.jg.zhang.androidtasktest.SecondActivity"/>
  22.                 <categoryandroid:name="android.intent.category.DEFAULT"/>
  23.             </intent-filter>
  24.         </activity>
  25.          <activityandroid:name="com.jg.zhang.androidtasktest.ThirdActivity"
  26.             android:label="@string/app_name">
  27.         </activity>
  28.     </application>
  29. </manifest>

由此可見,MainActivity和ThirdActivity都是標準的啟動模式,而SecondActivity的啟動模式為singleTask。 以下為這三個Activity的介面,很簡單,在MainActivity中點選按鈕啟動SecondActivity,在SecondActivity中點選按鈕啟動ThirdActivity。
以下為這三個activity的主要程式碼: MainActivity
  1. publicclass MainActivity extends Activity {  
  2.     privatestaticfinal String ACTIVITY_NAME = "MainActivity";  
  3.     privatestaticfinal String LOG_TAG = "xxxx";  
  4.     @Override
  5.     protectedvoid onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);  
  8.         findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {  
  9.             @Override
  10.             publicvoid onClick(View v) {  
  11.                 Intent intent = new Intent(MainActivity.this, SecondActivity.class);  
  12.                 startActivity(intent);  
  13.             }  
  14.         });  
  15.         int taskId = getTaskId();  
  16.         Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任務的id為: " +  taskId);  
  17.     }  


SecondActivity
  1. publicclass SecondActivity extends Activity {  
  2.     privatestaticfinal String ACTIVITY_NAME = "SecondActivity";  
  3.     privatestaticfinal String LOG_TAG = "xxxx";  
  4.     @Override
  5.     protectedvoid onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_second);  
  8.         findViewById(R.id.button2).setOnClickListener(new OnClickListener() {  
  9.             @Override
  10.             publicvoid onClick(View v) {  
  11.                     Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);  
  12.                     startActivity(intent);  
  13.             }  
  14.         });  
  15.         int taskId = getTaskId();  
  16.         Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任務的id為: " +  taskId);  
  17.     }  

ThirdActivity
  1. publicclass ThirdActivity extends Activity {  
  2.     privatestaticfinal String ACTIVITY_NAME = "ThirdActivity";  
  3.     privatestaticfinal String LOG_TAG = "xxxx";  
  4.     @Override
  5.     protectedvoid onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_third);  
  8.         int taskId = getTaskId();  
  9.         Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任務的id為: " +  taskId);  
  10.     }  

以上三個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屬性,如下所示:
  1. <activityandroid:name="com.jg.zhang.androidtasktest.SecondActivity"
  2.     android:launchMode="singleTask"
  3.     android:taskAffinity="com.jg.zhang.androidtasktest.second">
  4.    <intent-filter>
  5.        <actionandroid:name="com.jg.zhang.androidtasktest.SecondActivity"/>
  6.        <categoryandroid:name="android.intent.category.DEFAULT"/>
  7.    </intent-filter>
  8. lt;/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,該程式的介面和上一個類似,程式碼也類似,再此僅列出清單檔案。
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3.     package="com.jg.zhang.androidtasktest1"
  4.     android:versionCode="1"android:versionName="1.0">
  5.     <uses-sdkandroid:minSdkVersion="9"android:targetSdkVersion="17"/>
  6.     <application
  7.         android:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"
  8.         android:theme="@style/AppTheme">
  9.         <activity
  10.             android:name="com.jg.zhang.androidtasktest1.MainActivity"
  11.             android:label="com.jg.zhang.androidtasktest1.MainActivity">
  12.             <intent-filter>
  13.                 <actionandroid:name="android.intent.action.MAIN"/>
  14.                 <categoryandroid:name="android.intent.category.LAUNCHER"/>
  15.             </intent-filter>
  16.         </activity>
  17.         <activity
  18.             android:name="com.jg.zhang.androidtasktest1.OtherActivity"
  19.             android:label="com.jg.zhang.androidtasktest1.OtherActivity"
  20.             android:taskAffinity="com.jg.zhang.androidtasktest.second"
  21.             android:launchMode="singleTask">
  22.         </activity>
  23.     </application>
  24. </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。程式碼和軟體介面就不列出了,只列出清單檔案
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3.     package="com.jg.zhang.androidtasktest"
  4.     android:versionCode="1"
  5.     android:versionName="1.0">
  6.     <uses-sdkandroid:minSdkVersion="10"android:targetSdkVersion="17"/>
  7.     <applicationandroid:allowBackup="true"
  8.         android:icon="@drawable/ic_launcher"android:label="androidtasktest">
  9.         <activityandroid:name="com.jg.zhang.androidtasktest.MainActivity">
  10.             <intent-filter>
  11.                 <actionandroid:name="android.intent.action.MAIN"/>
  12.                 <categoryandroid:name="android.intent.category.LAUNCHER"/>
  13.             &