図で理解するActivityのスタック

図で理解するActivityのスタック


こんにちは,プログラマの保坂です。

今回は,Androidアプリ開発の上で重要な「Activity」のスタックについて,基礎的なところを解説してみたいと思います。関連して,複数のActivityによって構成される単位である「Task」や,Android 3.0以降のバージョンで利用可能な「Fragment」による画面遷移についてもここで紹介します。

以下では,ActivityやTaskが持つスタック構造に着目して,Androidアプリにおける画面遷移のバリエーションを見ていきます。できれば実際のアプリ開発で試してみるのが一番ですが,斜め読みでもなるべく内容が伝わるように,図の例を多めにしています。

TaskとActivity

Activityとは

Activityとは,Androidアプリの中核を成す概念で,アプリのライフサイクルや画面遷移に密接に関わるオブジェクトです。基本的には,Activity=アプリ上の1画面と考えても差し支えありません(ただし,Fragmentの登場でこの認識も変化しつつあります)。

通常は,AndroidManifest.xmlにおいてaction.MAINが設定されたActivityがエントリポイント(HOMEアプリから呼ばれる)となります。

Activityから別のActivityを呼び出す(startActivityをコールする)ことで,Activityがスタックにpushされていきます。また,ユーザーがBACKキーを押す(finishをコールする)と,このスタックからActivityがpopされます。

図1: Activityスタック


図1: Activityスタック

Taskとは

前述のActivityをひとまとめにしたものがTaskという概念です。通常は,同じアプリのActivityはすべて同じTaskに属します。

launchModeやIntentのフラグを変更しない限り,startActivityによって同じタスクにActivityがpushされていきます。

Task自体もまた,Activityと同様にスタックを形成します。

ユーザーがBACKキーを押すことによりTaskが空になると,ひとつ手前のTaskがForegroundになります。ユーザーがHOMEキーを押すと,ForegroundにあったTaskがBackgroundになり,BackgroundにあったHomeアプリのTaskがForegroundになります。

図2: Taskスタックの挙動


図2: Taskスタックの挙動

FLAG_ACTIVITY_NEW_TASKフラグの挙動

Activityを呼び出す際には,Intentオブジェクトを引数にしてstartActivityをコールします。このIntentオブジェクトには,いくつかのフラグを設定することができます。

ここでは,この中でも特に重要なフラグであるFLAG_ACTIVITY_NEW_TASKについて紹介します。

/* FLAG_ACTIVITY_NEW_TASK付きでActivityを呼び出す例 */
Intent intent = new Intent();
intent.setClass(getApplication(), Activity3.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

FLAG_ACTIVITY_NEW_TASK付きでstartActivityがコールされた場合,対象となるActivityと呼び出し元のActivityのタスク親和性(task affinity)を比較し,これらが異なる場合に限り新しいタスクを生成します。

タスク親和性とは,Activityの起動の際に同じタスク内で起動するか,新しくタスクを生成するかの判断に使われる文字列です。タスク親和性は,AndroidManifest.xmlによってカスタマイズ可能で,デフォルトではアプリのパッケージ名と同一となります。したがって,通常アプリ内のActivityを呼び出すのにFLAG_ACTIVITY_NEW_TASKを使っても,特に挙動は変わりません。

まとめると,次のようになります。

  • 同じタスク親和性を持つActivityをFLAG_ACTIVITY_NEW_TASK付きで呼び出しても,タスクは生成されない。
  • 異なるタスク親和性を持つActivityを起動するときに,FLAG_ACTIVITY_NEW_TASKを使うかどうかで挙動が変化する。
図3: NEW_TASKフラグによる挙動


図3: NEW_TASKフラグによる挙動

Intentに指定可能な他のフラグについて知るには,Google公式のAPI Referenceを読んでみてください。

ActivityのlaunchModeとは

AndroidManifest.xmlのactivity要素に設定できる属性の一種です。この値により,対象のActivityを呼び出すときの挙動が変わります。

以下,ひとつずつ挙動の違いを見ていきます。

<!-- launchModeをsingleInstanceに設定するAndroidManifest.xmlの例 -->
<activity
    android:name=".Activity1"
    android:label="@string/app_name"
    android:screenOrientation="portrait"
    android:launchMode="singleInstance" >
</activity>

launchMode=”standard”のとき

standardはデフォルトのモードです。

既にスタックにあるActivityの種類に関係なく,新しいActivityインスタンスが生成されて(onCreateが呼ばれる),スタックに積まれます。

launchMode=”singleTop”のとき

スタックのトップにあるActivityによって挙動が変わります。

スタックのトップに同じ種類のActivityがある場合,Activityインスタンスは生成されずに使いまわされます(onNewIntentが呼ばれる)。スタックのトップに違う種類のActivityがある場合,挙動はstandardと同じになります。

図4: singleTop


図4: singleTop

launchMode=”singleTask”のとき

既にスタック中に同じ種類のActivityがある場合,そのActivityよりも上にあるActivityをすべて終了(finish)します。その場合,既存のActivityインスタンスのonNewIntentが呼ばれます。

スタック中に同じ種類のActivityがなければ,挙動はstandardと同じになります。

図5: singleTask


図5: singleTask

launchMode=”singleInstance”のとき

singleInstanceが設定されたActivityは,常にそのActivityのみが含まれるTaskを構成します。

そのために,別のActivityから呼び出されるケースや,別のActivityを呼び出すケースにおいて,必要に応じてTaskを生成します。また,別のActivityから呼び出される際に,既に同じActivityが存在するのであれば,そのActivityが含まれるTaskがForegroundとなります。

図6: singleInstance


図6: singleInstance

Task・Activityの状態を確認する

なお,この記事を書くにあたって,手持ちのXperia GX(SO-04D)にて実際の挙動を確認しました。

確認にあたって使用したのが下記のコマンドです。

$ adb shell dumpsys activity activities

これだけだと情報量が多いので,次のようにして出力をフィルタするとよいでしょう。

$ adb shell dumpsys activity activities | grep "* TaskRecord\|* Hist"

以下に出力の一例を示します。

  * TaskRecord{425b6348 #121 A jp.qoncept.activitystack2}
    * Hist #6: ActivityRecord{422a2d98 jp.qoncept.activitystack2/.Activity3}
    * Hist #5: ActivityRecord{4197a3e8 jp.qoncept.activitystack2/.Activity2}
    * Hist #4: ActivityRecord{4177c190 jp.qoncept.activitystack2/.Activity1}
  * TaskRecord{4178ac50 #120 A jp.qoncept.activitystack1}
    * Hist #3: ActivityRecord{41c6be08 jp.qoncept.activitystack1/.Activity3}
    * Hist #2: ActivityRecord{41c58090 jp.qoncept.activitystack1/.Activity2}
  * TaskRecord{424a6d18 #119 A jp.qoncept.activitystack1}
    * Hist #1: ActivityRecord{417b5cf0 jp.qoncept.activitystack1/.Activity1}
  * TaskRecord{422c2960 #4 A com.sonyericsson.home}
    * Hist #0: ActivityRecord{422c15e0 com.sonyericsson.home/.HomeActivity}

dumpsys自体はAndroidの様々なシステム情報(メモリ使用率等)を出力する機能を持っていますので,覚えておくとなにかと便利です。

FragmentとBackStack

Fragmentは,Android 3.0から実装されたオブジェクトで,Activityを構成するビューコンポーネントやロジックをまとめたものです(Android 3.0未満のバージョンでFragmentを扱うには,Android Support Libraryが必要です)。Fragmentを使うことで,Activityのライフサイクルに依存するロジックをActivityから切り出すことができます。

また,Fragmentを管理するFragmentManagerに,Fragmentへの操作をスタック(BackStack)として記録する機能があります。これを使うと,まるでActivityにおけるstartActivity/finishのような画面遷移をFragmentで実装できます。

ここでは,そのBackStackを取り上げて利用例を見ていきます。

FragmentManagerのBackStack利用例

まず初めに,ActivityのUI上にインスタンス化したFragmentを追加します。

Fragmentの追加は,次のようなコードで実行可能です。

/* Fragmentを追加するコード */
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.fragmentContainer, new CustomFragment());
ft.commit();

続いて,さきほど追加したFragmentをreplaceし,この操作をBackStackに記録します。

Fragmentのreplaceは,次のようなコードで実行可能です。

/* FragmentをreplaceしてBackStackに記録するコード */
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragmentContainer, new Fragment1());
ft.addToBackStack(null);
ft.commit();

この状態でpopBackStackが実行されると,前回のreplace操作が取り消されます。

BackStackのpopは,次のようなコードで実行可能です。

/* BackStackをpopするコード */
FragmentManager fm = getFragmentManager();
fm.popBackStack();

ただし,明示的に実装をしなくても,端末のBACKボタンを押すだけでもpopBackStackが実行されます。これを使えば,FragmentのみでActivityの遷移とほぼ同じUIを実現できます。

図7: BackStackの挙動


図7: BackStackの挙動

おわりに

少し長くなりましたが,TaskとActivityの仕組み,そしてFragmentを使った画面遷移について解説しました。ここまでを抑えておけば,Intentに指定可能な他のフラグについても,比較的素直に理解ができるのではないかと思います。

やや複雑な部分もありますが,起動モードやフラグを使いこなして,使いやすいAndroidアプリを目指したいものです。

Fragmentでできることは他にも色々ありますが,今回紹介したような方法で,Activityで実現されていたようなスタックによる画面遷移をFragmentで実装することができます。Activityは1つのみで,画面遷移をFragmentへの操作だけで完結させるようなアプリも作れるかもしれませんね(それが設計として正しいのかはともかく……)。

また次回をお楽しみに!