2012-01-23

Android Launch Mode

Task 進階

Android Task、Home 鍵與 Back 鍵 裡講的 Task 預設行為應該滿足大部分 App 的需求,除此之外,Task 也可以應付一些特別的需求:
  • 在新的 Task 裡啟動 Activity,而不是在目前的 Task 上堆疊
  • 啟動一個已經啟動過的 Activity 時,不要產生新的 instance,而是回到既有的 instance
  • 當離開 Task 時,清掉 Task 裡所有的 Activity,除了最後一個 Activity

Launch Mode

Launce Mode 用來設定如何啟動 Activity,尤其是與 Task 的關係

Launch Mode 可以在 Manifest 與 Intent Flag 裡做設定,如果兩邊都有做相同屬性的設定,Intent Flag 會覆蓋 Mainfest 的設定,這是指兩邊共有的屬性,Manifest 有些屬性是 Intent Flag 沒有的,反之亦然,Intent Flag 有些屬性是 Manifest 沒有的。

使用 Manifest 或 Intent Flag 最大的差異在於,Manifest 是由被請求者設定,而 Intent Flag 是由請求者設定,舉例來說,Activity A (請求者)啟動 Activity B(被請求者),B 可以透過 Manifest 決定自身的 Lanuce Mode,而 A 可以透過 Intent Flag 決定或覆蓋 B 的 Launch Mode。

透過 Manifest 設定 launchMode:
  • standard - 預設值,同一個 Activity 可以產生多個 instance,分屬於不同 Task,或同一個 Task 可以有多個 instance。

    舉例說明:假設目前堆疊為 ABC,Intent 呼叫 C,C 的 launchMode 為 standard,堆疊就會變成 ABCC,C 有兩個 instance。

  • singleTop - 同 standard 的 instance 邏輯「同一個 Activity 可以產生多個 instance,分屬於不同 Task,或同一個 Task 可以有多個 instance」,唯一不同的地方在於,當某個 Activity 在 Task 頂端時,此時若有 Intent 要到同一個 Activity 時,並不會重新產生一個 instance,而是使用目前的 instance,只是會先呼叫該 instance的 onNewIntent() 表示有新的 Intent 進來了。

    舉例說明:假設目前堆疊為 ABC,Intent 呼叫 C,C 的 launchMode 為 singleTop,堆疊就會維持 ABC,並呼叫 C 的 onNewIntent();若 Intent 呼叫的是 B,不管 B 的 launchMode 為 standard 或者 singleTop,堆疊都會變成 ABCB,B 有兩個 instance。

    有幾點要特別注意:

    按下 Back 鍵可以回到前一個 Activity,但是當目前的 Activity 因為 singleTop 的 launchMode 而呼叫過 onNewInent() 時,按下 Back 鍵並不是回到 onNewIntent() 前的狀態,而是回到前一個 Activity,以上面的例子說明,就是回到 B。

    假設目前堆疊為 AB,Intent 呼叫 B,B 的 launchMode 為 singleTop,堆疊雖然會維持 AB,但是發生了很多事,首先會呼叫 B 的 onPause(),再呼叫 B 的 onNewIntent(),最後呼叫 B 的 onResume(),雖然是停在同一個 Activity,之所以會呼叫 onPause() 的原因是每個 Activity 在接收一個新的 Intent 之前都會先呼叫 onPause()。

    再來就是 intent,onNewIntent() 會傳進一個 intent,這個 intent 不同於 Activity 裡 getIntent() 得到的 intent,因為 getIntent() 紀錄的是當初啟動這個 instance 的 intent,而 onNewIntent() 傳進來的則是由於 singleTop 而導進來的 intent,可以在 onNewIntent() 裡呼叫 setIntent() 來更新 Activity 所持有的 intent。

  • singleTask - 只會有一個 instance 存在,啟動時若沒有任何 instance 存在,則新增一個 instance 放到目前的 Task 裡,若已經有 instance 存在,即使不在最上層,也會直接切過去,並呼叫 onNewIntent(),在該 instance 上方的堆疊全部銷毀。

    舉例說明:假設目前堆疊為 ABC,Intent 呼叫 B,B 的 launchMode 為 singleTask,堆疊就會變成 AB,C 活生生被拔掉,按下 Back 鍵會到 A;若 Intent 呼叫 D,D 的 launchMode 為 singleTask,而目前的堆疊裡沒有 D,那會在目前 Task 放上 D。

    singleTask 可以應用到主畫面的設計上,主畫面加上 singleTask,那麼在任一頁只要呼叫主畫面的 Activity 就可以直接回到主畫面。

  • singleInstance - 延伸自 singleTask,唯一不同是 singleInstance 所在的 Task 容不下其他 Activity,所有 singleInstance Activity 啟動的 Activity 都會放到別個 Task 裡。

    舉例說明:假設目前堆疊為 AB,Intent 呼叫 C,C 的 launceMode 為 singleInstance,會產生一個新的堆疊放上 C,原堆疊 AB 維持不動並移到背景,此時若再呼叫 C,則會留在新堆疊的 C 上,不會再產生新的堆疊或 C instance,若再呼叫另一個 launceMode 同為 singleInstance 的 D,會產生第三個堆疊並放上 D,這是若再呼叫 C,則是回到第二個堆疊的 C,最後若按下 Back 鍵會離開 C 回到 D,再按下 Back 鍵則會離開 C 回到 B,最後回到 A。

    一個有趣的狀況,假設目前堆疊為 AB,先呼叫 launceMode 為 singleInstance 的 C,這時若呼叫 launceMode 不為 singleInstance 的 D,也就是其他三種 launceMode,會回到第一個堆疊並放上 D,第一個堆疊就變成 ABD;另一個有趣的狀況,雖然呼叫的順序為 ABCD,但是在 D 按下 Back 鍵是回到同一堆疊的 B,而不是第二個堆疊的 C,要等到第一個堆疊完全 Back 之後,才會回到第二個堆疊的 C。

    因為 singleInstance 會不斷地產生新的堆疊,所以要注意堆疊的轉換,當目前的堆疊消失後會回到最近移到背景的堆疊,也就是說,堆疊間的移動也是遵守後進先出的原則。

有看沒有懂?寫程式是看看就知道,先看畫面:


總共有八個 Activity,分屬四種 launchMode,每種 launchMode 兩個 Activity,Main Activity 是 A,「From」表示哪個 Activity 傳送 Intent 的,「Stack」表示同一個 Task 的堆疊。

a.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Go to:" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/A"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="Standard A" />

        <Button
            android:id="@+id/B"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="Standard B" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/C"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="SingleTop C" />

        <Button
            android:id="@+id/D"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="SingleTop D" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/E"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="SingleTask E" />

        <Button
            android:id="@+id/F"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="SingleTask F" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/G"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="SingleInstance G" />

        <Button
            android:id="@+id/H"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onClick"
            android:text="SingleInstance H" />
    </LinearLayout>

    <TextView
        android:id="@+id/from"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/newIntent"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/stack"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
因為每個 Activity 的程式雷同,所以抽出一個父類別。

ParentActivity.java
public abstract class ParentActivity extends Activity {

    private static final String EXTRA_FROM = "from";
    private static final String EXTRA_STACK_MAP = "stackMap";
    private TextView fromTv;
    private TextView newIntentTv;
    private TextView stackTv;
    private HashMap<String, String> stackMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.a);
        this.fromTv = (TextView) this.findViewById(R.id.from);
        // 一開始就寫死,固定為啟動該 Activity 的 Activity,啟動並非重新進來
        this.fromTv.setText("From: "
                + this.getIntent().getStringExtra(ParentActivity.EXTRA_FROM));
        // 給 singleTop、singleTask、singleInstance 使用,表示進入已有的 instance 
        this.newIntentTv = (TextView) this.findViewById(R.id.newIntent);
        // 堆疊,依 Task id 分堆
        this.stackTv = (TextView) this.findViewById(R.id.stack);
    }

    @Override
    protected void onResume() {
        super.onResume();
        this.log("onResume");
        // 取出全部 Task id 的堆疊
        this.stackMap = (HashMap<String, String>) this.getIntent().getSerializableExtra(
                ParentActivity.EXTRA_STACK_MAP);
        if (this.stackMap == null) {
            this.stackMap = new HashMap<String, String>();
        }
        // 找出目前 Task Id 的堆疊
        String stack = this.stackMap.get("stack" + this.getTaskId());
        if (stack == null) {
            stack = "";
        }
        // 新加入的堆疊
        String newStack = this.toString() + ", Task: " + this.getTaskId()
                + "\n";
        // 若新加入的堆疊已存在既有的堆疊,且在最上方,則不加入新的堆疊
        // 因為可能是 Back 鍵回來或者 onNewIntent() 進來,這兩種情形並不會產生新的堆疊
        if (stack.indexOf(newStack) != 0) {
            stack = newStack + stack;
        }
        // 輸出堆疊到畫面
        this.stackTv.setText("Stack:\n" + stack);
        // 紀錄堆疊到 Intent 裡,供各 Activity 使用
        this.stackMap.put("stack" + this.getTaskId(), stack);
    }

    @Override
    protected void onPause() {
        super.onPause();
        this.log("onPause");
    }

    public void onClick(View v) {
        this.log("onClick");
        Intent it = new Intent();
        switch (v.getId()) {
        case R.id.A:
            it.setClass(this, A.class);
            break;
        case R.id.B:
            it.setClass(this, B.class);
            break;
        case R.id.C:
            it.setClass(this, C.class);
            break;
        case R.id.D:
            it.setClass(this, D.class);
            break;
        case R.id.E:
            it.setClass(this, E.class);
            break;
        case R.id.F:
            it.setClass(this, F.class);
            break;
        case R.id.G:
            it.setClass(this, G.class);
            break;
        case R.id.H:
            it.setClass(this, H.class);
            break;
        }
        it.putExtra(ParentActivity.EXTRA_FROM, this.getClass().getSimpleName());
        it.putExtra(ParentActivity.EXTRA_STACK_MAP, this.stackMap);
        this.startActivity(it);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        this.log("onNewIntent");
        // 這邊要用傳進來的 intent,不可以用 getIntent(),因為這兩個是不一樣的
        // getIntent() 是指一開始建立這個 instance 時傳進來的 intent
        // 而傳進來的 intent 則是因為 singleTop/singleTask 再次傳進來的 intent
        // 若要修改 getIntent(),可以用 setIntent(intent)
        this.newIntentTv.setText("onNewIntent: "
                + intent.getStringExtra(ParentActivity.EXTRA_FROM));
    }

    public abstract void log(String msg);

}
A.java
public class A extends ParentActivity {

    private static final String TAG = "A";

    @Override
    public void log(String msg) {
        Log.d(TAG, msg);
    }
}
B.java 到 H.java 同 A.java,只有 Log 的 TAG 不一樣而已。

AndroidManifest.xml
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
    android:label="A"
    android:name=".A" >
    <intent-filter >
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity
    android:label="B"
    android:name=".B" >
</activity>
<activity
    android:label="C"
    android:launchMode="singleTop"
    android:name=".C" >
</activity>
<activity
    android:label="D"
    android:launchMode="singleTop"
    android:name=".D" >
</activity>
<activity
    android:label="E"
    android:launchMode="singleTask"
    android:name=".E" >
</activity>
<activity
    android:label="F"
    android:launchMode="singleTask"
    android:name=".F" >
</activity>
<activity
    android:label="G"
    android:launchMode="singleInstance"
    android:name=".G" >
</activity>
<activity
    android:label="H"
    android:launchMode="singleInstance"
    android:name=".H" >
</activity>
</application>

相關文章

沒有留言:

張貼留言