Activity啟動模式與任務棧(Task)全面深入記錄(上)
任務棧簡單入門
最近又把兩本進階書看了一遍,但總感覺好記性不如爛筆頭,所以還是決定通過部落格記錄一下,我們將分兩篇來全面深入地記錄Activity 啟動模式與任務棧的內容。
android任務棧簡單瞭解
1. android任務棧又稱為Task,它是一個棧結構,具有後進先出的特性,用於存放我們的Activity元件。
2. 我們每次開啟一個新的Activity或者退出當前Activity都會在一個稱為任務棧的結構中新增或者減少一個Activity元件,因此一個任務棧包含了一個activity的集合, android系統可以通過Task有序地管理每個activity,並決定哪個Activity與使用者進行互動:只有在任務棧棧頂的activity才可以跟使用者進行互動。
3.
4. 需要注意的是,一個App中可能不止一個任務棧,某些特殊情況下,單獨一個Actvity可以獨享一個任務棧。還有一點就是一個Task中的Actvity可以來自不同的App,同一個App的Activity也可能不在一個Task中。
嗯,目前android任務棧的概念我們就大概瞭解到這。下面我們主要還是來聊聊android的4種啟動模式。
Activity的啟動模式
為什麼需要Activity的啟動模式?
我們在開發專案的過程中,一般都需要在本應用中多個Activity元件之間的跳轉,也可能需要在本應用中開啟其它應用的可複用的Activity。如我們可能需要跳轉到原來某個Activity例項,此時我們更希望這個Activity可以被重用而不是建立一個新的 Activity,但根據Android系統的預設行為,確實每次都會為我們建立一個新的Activity並新增到Task中,這樣android系統是不是很傻?還有一點就是在我們每開啟一次頁面加入到任務棧Task中後,一個Activity的資料和資訊狀態都將會被保留,這樣會造成資料冗餘, 重複資料太多, 最終還可能導致記憶體溢位的問題(OOM)。為了解決這些問題,android系統提供了一套Activity的啟動模式來修改系統Activity的預設啟動行為。目前啟動模式有四種,分別是standard,singleTop,singTask和singleInstance,接下來我們將分別介紹這四種模式。
Activity的4種啟動模式
- Standard 模式
又稱為標準模式,也是系統的預設模式(可以不指定),在這樣模式下,每啟動一個Activity都會重新建立一個Activity的新例項,並且將其加入任務棧中,而且完全不會去考慮這個例項是否已存在。我們通過圖解來更清晰地瞭解Standard模式:
通過上圖,我們可以發現,這個過程中,在standard模式下啟動了三次MainActivity後,都生成了不同的新例項,並新增到同一個任務棧中。這個時候Activity的onCreate、onStart、onResume方法都會被呼叫。
- singleTop 模式
又稱棧頂複用模式,顧名思義,在這種模式下,如果有新的Activity已經存在任務棧的棧頂,那麼此Activity就不會被重新建立新例項,而是複用已存在任務棧棧頂的Activity。這裡重點是位於棧頂,才會被複用,如果新的Activity的例項已存在但沒有位於棧頂,那麼新的Activity仍然會被重建。需要注意的是,Activity的onNewIntent方法會被呼叫,方法原型如下:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
}
通過此方法的引數,我們可以獲取當前請求的相關資訊,此時Activity的onCreate、onStart方法不會被呼叫,因為Activity並沒有被重建。同理,我們通過圖解來協助我們更清晰的理解singleTop模式:
從上圖我們可以看出,當需要新建立的MainActivity位於棧頂時,MainActivity並沒有重新建立。下面我們再來看看新建立的MainActivity沒有位於棧頂的情況。
嗯,這就是singTop模式。這種模式通常比較適用於接收到訊息後顯示的介面,如qq接收到訊息後彈出Activity介面,如果一次來10條訊息,總不能一次彈10個Activity,是吧?再比如新聞客戶端收到了100個推送,你每次點一下推送他都會進入某個activiy介面(顯示新聞只用一個activity,只是內容不同而已),這時也比較適合使用singleTop模式。
- singleTask 模式
又稱為棧內複用模式。這是一種單例模式,與singTop點類似,只不過singTop是檢測棧頂元素是否有需要啟動的Activity,而singTask則是檢測整個棧中是否存在當前需要啟動的Activity,如果存在就直接將該Activity置於棧頂,並將該Activity以上的Activity都從任務棧中移出銷燬,同時也會回撥onNewIntent方法。情況如下圖:
從圖中可以看出,當我們再次啟動MainActivity時,由於MainActivity位於棧中,所以系統直接將其置於棧頂,並移除其上方的所有Activity。當然如果所需要的MainActivity不存在棧中,則會建立新的Activity並新增到棧中。singleTask 模式比較適合應用的主介面activity(頻繁使用的主架構),可以用於主架構的activity,(如新聞,側滑,應用主介面等)裡面有好多fragment,一般不會被銷燬,它可以跳轉其它的activity 介面再回主架構介面,此時其他Activity就銷燬了。當然singTask還有一些比較特殊的場景這個我們後面會一一通過情景程式碼分析。
- singleInstance 模式
在singleInstance模式下,該Activity在整個android系統記憶體中有且只有一個例項,而且該例項單獨尊享一個Task。換句話說,A應用需要啟動的MainActivity 是singleInstance模式,當A啟動後,系統會為它建立一個新的任務棧,然後A單獨在這個新的任務棧中,如果此時B應用也要啟用MainActivity,由於棧內複用的特性,則不會重新建立,而是兩個應用共享一個Activity的例項。如下圖所示:
從圖中我們可以看到最終AB應用都共享一個singleInstance模式的MainActivity,也沒有去重新建立。到此Activity的四種啟動模式我們都介紹完了,下面我們接著來聊聊怎麼使用啟動模式。
Activity啟動模式的使用方式
前面我們說了那麼多,那麼我們該如何給Activity指定啟動模式呢?事實上共有如下兩種方式:
1.通過AndroidMenifest.xml檔案為Activity指定啟動模式,程式碼如下:
<activity android:name=".ActivityC"
android:launchMode="singleTask" />
2.通過在Intent中設定標誌位(addFlags方法)來為Activity指定啟動模式,示例程式碼如下:
Intent intent = new Intent();
intent.setClass(ActivityB.this,ActivityA.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
那麼標誌位是是什麼呢?接下來我們就來了解一些常用的標誌位
Intent Flag 啟動模式
這裡我們主要介紹一下一些常用的Activity的Flag,因為Activity的Flag比較多,我們知道一些常用的就夠了,遇到比較特殊的還是查查官網文件吧。
- Intent.FLAG_ACTIVITY_NEW_TASK
該標誌位表示使用一個新的Task來啟動一個Activity,相當於在清單檔案中給Activity指定“singleTask”啟動模式。通常我們在Service啟動Activity時,由於Service中並沒有Activity任務棧,所以必須使用該Flag來建立一個新的Task。我們來重現一下這個錯誤,建立一個Service服務,並在onCreate方法中啟動Activity,程式碼如下:
public class ServiceT extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Intent i =new Intent(getApplicationContext(),ActivityD.class);
startActivity(i);
}
}
啟動應用並啟動Service服務,後報錯如下:
從異常資訊我們可以看出,提示我們新增Intent.FLAG_ACTIVITY_NEW_TASK
標誌位,所以我們程式碼必須改成如下:
public class ServiceT extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Intent i =new Intent(getApplicationContext(),ActivityD.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
}
Intent.FLAG_ACTIVITY_SINGLE_TOP
該標誌位表示使用singleTop模式來啟動一個Activity,與在清單檔案指定android:launchMode="singleTop"
效果相同。Intent.FLAG_ACTIVITY_CLEAR_TOP
該標誌位表示使用singleTask模式來啟動一個Activity,與在清單檔案指定android:launchMode="singleTask"
效果相同。Intent.FLAG_ACTIVITY_NO_HISTORY
使用該模式來啟動Activity,當該Activity啟動其他Activity後,該Activity就被銷燬了,不會保留在任務棧中。如A-B,B中以這種模式啟動C,C再啟動D,則任務棧只有ABD。Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
使用該標識位啟動的Activity不新增到最近應用列表,也即我們從最近應用裡面檢視不到我們啟動的這個activity。與屬性android:excludeFromRecents="true"
效果相同。
啟動模式中singleTask的特殊情景
前面我們在分析singleTask模式時,提到過singleTask模式有些比較特殊的場景,現在我們就來了解了解它們。
特殊情景一:現在我們假設有如下兩個Task棧,分別為前臺任務棧和後臺任務棧
從圖中我們看出前臺任務棧分別為AB兩個Activity,後臺任務棧分別為CD兩個任務棧,而且其啟動模式均為singleTask,此時我們先啟動CD,然後再啟動AB,再有B啟動D,此時後臺任務棧便會被切換到前臺,而且這個時候整個後退列表就變成了ABCD,請注意我們這裡強調的是後退列表,而非棧合併。因此當用戶點選back鍵時,列表中的Activity會依次按DCBA順序出棧,如下圖所示:
這裡我們通過兩個應用ActivityTask和ActivityTask2來測試重現這個現象。因為兩個是不同的應用所以啟動時所在的棧也是不同。我們先啟動ActivityTask2的應用,其ActivityC和ActivityD都是singleTask模式,然後再啟動應用ActivityTask,此時ActivityC和ActivityD所在任務棧會被退居後臺,而開啟的ActivityA和ActivityB會在前臺,而且都是預設模式。我們通過 adb shell dumpsys activity activities
命令檢視此時棧的情況:
我們可以看到由兩個棧,分別為id=222且棧名為“com.cmcm.activitytask”的任務棧其包含ActivityA和ActivityB(下面簡稱AB,棧名一般預設和包名相同),另外一個任務棧,id=221,棧名為“com.cmcm.activitytask2”,其包含ActivityC和ActivityD(下面檢測CD)。現在我們通過ActivityB去啟動ActivityD,然後按back鍵回退。B呼叫D程式碼如下:
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
/**
* Created by zejian
* Time 16/7/23.
* Description:
*/
public class ActivityB extends Activity {
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_b);
btn= (Button) findViewById(R.id.main);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
ComponentName cn = new ComponentName("com.cmcm.activitytask2", "com.cmcm.activitytask2.ActivityD");
intent.setComponent(cn);
startActivity(intent);
}
});
}
}
執行結果如下:
我們可以看到包含CD的任務棧被提前的,雖然CD隔開了,但是我們從id和棧名可以發現他們是同一個棧,而AB所在的棧則在CD所在棧的後面,所以此時我們按back回退時,退出順序是這樣的D->C->B->A,動態圖如下:
到這裡我們就應該更加清晰的瞭解情景一的現象了。瞭解這點有什麼用呢,這可以使用我們更好地去管理我們的任務棧,而不會導致棧混亂是進入一些使用者本來就不需要介面,影響使用者體驗。
特殊情景二:
如果上面B不是請求啟動D而是請求啟動C,那麼又會是什麼情況呢?其實這個時候任務棧退出列表變成C->B->A,其實原因很簡單,singleTask模式的ActivityC切換到棧頂時會導致在他之上的棧內的Activity出棧。同樣我們還是使用上面的程式碼,把B啟動D改為B啟動C,那麼此時B未啟動C時任務棧的情況如下:
我們仍然可以看到兩個任務棧,分別為id=242,棧名“com.cmcm.activitytask”的Task,包含ActivityA和ActivityB;id=241,棧名“com.cmcm.activitytask2”的Task,包含ActivityC和ActivityD。此時我們通過B啟動C後棧的情況變成如下情況
因此,棧的退出列表就變成了C->B->A了,如下圖所示:
動態圖如下:
到此我們對SingleTask模式又有了更深入的理解,但是我們發現上面的例子使用的是兩個應用,所以才會有不同的任務棧,那麼我們能不能在一個應用中存在多個不同的任務棧呢(暫時不考慮singleInstance 模式)?答案當然是肯定的啦,這就需要通過taskAffinity屬性來設定不同的任務棧名稱,不過這點將放在下篇來記錄,本篇就先到這裡告一段落哈。