1. 程式人生 > >Activity啟動模式與任務棧(Task)全面深入記錄(上)

Activity啟動模式與任務棧(Task)全面深入記錄(上)

任務棧簡單入門

  最近又把兩本進階書看了一遍,但總感覺好記性不如爛筆頭,所以還是決定通過部落格記錄一下,我們將分兩篇來全面深入地記錄Activity 啟動模式與任務棧的內容。

android任務棧簡單瞭解

1. android任務棧又稱為Task,它是一個棧結構,具有後進先出的特性,用於存放我們的Activity元件。
2. 我們每次開啟一個新的Activity或者退出當前Activity都會在一個稱為任務棧的結構中新增或者減少一個Activity元件,因此一個任務棧包含了一個activity的集合, android系統可以通過Task有序地管理每個activity,並決定哪個Activity與使用者進行互動:只有在任務棧棧頂的activity才可以跟使用者進行互動。
3.

在我們退出應用程式時,必須把所有的任務棧中所有的activity清除出棧時,任務棧才會被銷燬。當然任務棧也可以移動到後臺, 並且保留了每一個activity的狀態. 可以有序的給使用者列出它們的任務, 同時也不會丟失Activity的狀態資訊。
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屬性來設定不同的任務棧名稱,不過這點將放在下篇來記錄,本篇就先到這裡告一段落哈。