起動されたActivityはAndroidの中で「スタックとタスク」という仕組みで管理されます。
LaunchModeはActivityを「スタックとタスク」の中へ配置する方法を決めます。
このLaunchModeがとても複雑で、アプリ開発で常に混乱を招く内容です。
なので、まとめました。
スタックとタスク
Androidは画面を切り替える
アプリケーションがユーザにより多くの情報を伝えたい時、
Windowsのアプリは新たなウィンドウを開いて横に並べることが可能です。
画面が広いですから、表示に余裕が多く残っているからです。
一方、Androidのアプリは新たなウィンドウを開いて横に並べることはしません。
画面が狭いですから、一つの情報の表示に手一杯で余裕が無いからです。
なので、画面を切り替えるという手法をとります。伝えたい情報が発生したら、それを表示する画面に切り替えるというわけです。
スタックとは
Androidは情報を表示する一つの画面を一つのActivityが受け持っています。
伝えたい情報が発生したらstartActicity( )で新たなActivityを開いて画面を切り替えます。
そして、Acitivityが必要なくなったらfinish( )で閉じて元の画面に戻ります。
図はその様子を示しています。
内部では、startActivity( )を実行するとActivityのインスタンスは既存のActivityの上に積み上げられます。積み上げられた最上位のAcitvityをフォアグラウンドといい、フォアグラウンドが現在表示されているActivityです。
フォアグラウンドよりも下位をバックグラウンドといい、表示が隠されているActivityです。
フォアグラウンドのActivityが終了されると上から取り去られて、バックグラウンドの先頭がフォアグラウンドへ移行します。
このように「積み上げる・上から取り去る」という管理方法をスタック構造といいます。
タスクとは
スタック構造で管理される一連のActivityの固まりをAndroidではタスクと呼んでいます。
タスク内のActivityは、アプリを起動した時の最初のActivity(ルートアクティビティ)から派生したものです。なので、アプリとタスクは、ほぼ等しい存在です(厳密にはアプリは一つ以上のタスクで構成される)。
このタスクもスタック構造で管理されています。
図はその様子を示しています。
アプリを起動するとタスクは既存のタスクの上に積み上げられます。スタック構造の最上位のタスクが現在表示されているアプリです。これにより新しいアプリに表示が切り替わります。
アプリの利用が終了されると上から取り去られて、バックグラウンドの先頭のタスクが最上位に移動します。これにより元のアプリに表示が戻ります。
スタックとタスクを確認
Activityとタスクのスタック構造を制御することで、Androiは表示対象を切り替えながら動作しています。
現状のスタックとタスクの状態はadb shellのdumpsys activityコマンドで確認できます。
$ dumpsys activity activities | grep -B 1 "Run #[0-9]*:" TaskRecord{19ad1d6 #56 A=com.example.myapp.Alpabet U=0 StackId=1 sz=4} Run #5: ActivityRecord{12e680 u0 com.example.myapp/com.example.myapp.SubActivity__D t56} Run #4: ActivityRecord{f60db5a u0 com.example.myapp/com.example.myapp.SubActivity__C t56} Run #3: ActivityRecord{21422e4 u0 com.example.myapp/com.example.myapp.SubActivity__B t56} Run #2: ActivityRecord{c0b8b8f u0 com.example.myapp/com.example.myapp.MainActivity_A t56} TaskRecord{37e22e8 #50 A=com.google.android.deskclock U=0 StackId=1 sz=1} Run #1: ActivityRecord{b615158 u0 com.google.android.deskclock/com.android.deskclock.DeskClock t50} TaskRecord{a67f7a6 #49 A=com.android.camera.CameraActivity U=0 StackId=1 sz=1} Run #0: ActivityRecord{ec129f3 u0 com.android.camera2/com.android.camera.CameraLauncher t49} -- TaskRecord{8c1afff #7 I=com.android.launcher3/.Launcher U=0 StackId=0 sz=1} Run #1: ActivityRecord{b1f5b83 u0 com.android.launcher3/.Launcher t7} TaskRecord{eb82b1e #10 A=com.android.systemui U=0 StackId=0 sz=1} Run #0: ActivityRecord{55b4fc u0 com.android.systemui/.recents.RecentsActivity t10}
Activityとタスクはスタック構造の上位から順に表示されています。例の場合、最上位のタスクは「#56」で、そのタスクのフォアグラウンドActivityは「#5」であることが読み取れます。
LaunchMode
「スタックとタスク」の章はActivityを上に追加するだけの単純な配置方法しか出てきませんでした。
これ以外にもLaunchModeを指定して、配置方法を色々と変えることが出来ます。
LaunchModeを指定する方法は2つあります。
- AndroidManifestにLaunchMode=”モード名”で指定(静的な指定)
- Intent#setFlags(モードフラグ)で指定(動的な指定)
AndroidManifestにLaunchMode=”モード名”で指定
AndroidManifestで指定する方法は静的な指定です。
Activityの属性として指定するので、起動されるActivityは毎回この属性に従います。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <application ... <activity android:name=".MainActivity_A" android:taskAffinity="com.example.myapp.Alpabet" android:launchMode="standard"> ... </activity> <activity android:name=".SubActivity__B" android:taskAffinity="com.example.myapp.Alpabet" android:launchMode="standard"/> ... <activity android:name=".MainActivity_1" android:taskAffinity="ccom.example.myapp.Number" android:launchMode="standard"/> </application> </manifest>
表は各モードの配置方法を表しています。
各モードに期待される基本な配置のみを記載しています。すべてIntent.setFlags()のフラグ指定(後述)がない場合です。
※配置方法はスタックとタスクの状態やIntent#setFlags(XXX)の指定により変わってきます。詳細は「LaunchMode=”XXX”とIntent#setFlags(XXX)の併用」を参照してください。
モード名 | 配置先タスク | 配置方法 |
---|---|---|
standard | 起動元のActivityを 含むタスク | 新しいインスタンスをスタックの最上位に配置 |
singleTop | 起動元のActivityを 含むタスク | タスクの先頭に同じインスタンスを重複して配置しない (既存のインスタンスを再利用する) |
singleTask | 起動元のActivityを 含むタスク | タスクの中に同じインスタンスを重複して配置しない (スタックの上位は削除、既存のインスタンスを再利用する) |
※FLAG_ACTIVITY_NEW_TASKフラグが自動で付加される |
||
singleInstance | 起動元のActivityを 含むタスク | タスクの中にインスタンスを1つだけ配置 (タスクは異なるが同一のtaskAffinityを持つ) |
※FLAG_ACTIVITY_NEW_TASKフラグが自動で付加される |
||
※onCreate Activityのインスタンスが新規に作成されて、ライフサイクルがonCreate~onResumeへ遷移する ※onNewIntent() Activityのインスタンスが再利用されて、ライフサイクルがonNewIntent~onResumeへ遷移する この時、Intentが新しいAcitivityのものへ置き換わる |
配置の基本は、配置先タスクが「起動元のActivityを含むタスク」になります。つまり、startActivity()を実行したActivityと同じタスクに積み上げられます。
Intent#setFlags(モードフラグ)で指定
Intent#setFlags()で指定する方法は動的な指定です。
Activityを起動するタイミングで指定するので、起動されるActivityのみがフラグに従います。
class SubActivity__C : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... findViewById<Button>(R.id.button).setOnClickListener { startActivity( Intent(this, SubActivity__D::class.java) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ) } ... } }
表は各フラグの配置方法の違いです。
各フラグに期待される基本的な配置のみを記載しています。すべてLaunchMode=”standard”を指定(前述)した場合です。
※配置方法はスタックとタスクの状態やLaunchMode=”XXX”の指定により変わってきます。詳細は「LaunchMode=”XXX”とIntent#setFlags(XXX)の併用」を参照してください。
フラグ名 | 廃止先タスク | 配置方法 |
---|---|---|
フラグ無し | 起動元のActivityを 含むタスク | 新しいインスタンスをスタックの最上位に配置 |
NEW_TASK | 新規作成したタスク | 新しいインスタンスを新規作成したタスクへ配置 (2つのタスクは異なるtaskAffinityを持つ) |
SINGLE_TOP | 起動元のActivityを 含むタスク | タスクの先頭に同じインスタンスを重複して配置しない (既存のインスタンスを再利用する) |
CLEAR_TOP | 起動元のActivityを 含むタスク | タスクの中に同じインスタンスを重複して配置しない (スタックの上位は削除、新しいインスタンスを作成する) |
※onCreate Activityのインスタンスが新規に作成されて、ライフサイクルがonCreate~onResumeへ遷移する ※onNewIntent() Activityのインスタンスが再利用されて、ライフサイクルがonNewIntent~onResumeへ遷移する この時、Intentが新しいAcitivityのものへ置き換わる |
配置の基本は、配置先タスクが「起動元のActivityを含むタスク」になります。つまり、startActivity()を実行したActivityと同じタスクに積み上げられます。
ただし、FLAG_ACTIVITY_NEW_TASKフラグを持ち、taskAffinity(タスクの親和性、後述)が指定されたActivityは新規作成したタスクが配置先になります。
taskAffinity(タスクの親和性)
taskAffinityはActivityに付けられた「なかよしグループ名」のようなものです。
タスク名ではありません。
taskAffinityはAndroidManifestで指定を行います。
デフォルトはパッケージ名です。なので、何も指定しなければパッケージ名になります。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <application ... <activity android:name=".MainActivity_A" android:taskAffinity="com.example.myapp.Alpabet" android:launchMode="standard"> ... </activity> <activity android:name=".SubActivity__B" android:taskAffinity="com.example.myapp.Alpabet" android:launchMode="standard"/> ... <activity android:name=".MainActivity_1" android:taskAffinity="ccom.example.myapp.Number" android:launchMode="standard"/> </application> </manifest>
taskAffinityの効果が発揮されるのはFLAG_ACTIVITY_NEW_TASKフラグが指定された時です。
通常のActivityの配置先は「起動元のActivityを含むタスク」になります。
しかし、FLAG_ACTIVITY_NEW_TASKフラグを持ち、起動元のActivityを含むタスクとtaskAffinityが異なっているActivityは、新規作成したタスクが配置先になります。
ちなみに、taskAffinityは一意な名前(世界でただ一つ)を付ける必要があります。従って、パッケージ名を頭に付けるのが通例のようです。
LaunchMode=”XXX”とIntent#setFlags(XXX)の併用
Activityの配置方法はLaunchModeだけでなく、現状のスタックとタスクの状態に影響されて変わってきます。
組み合わせが多くて複雑です。
以下に色々な組み合わせを図にまとめました。参考にしてください。
図は1行目がActivityの追加動作(Sub3からstartActivity()を実行)、2行目移行が追加後のスタックとタスクの状態です。各行の右端に併用したフラグ名が書かれています。
LaunchMode=”standard”の場合
standardはデフォルトです。
AndroidManifestに何も指定しなければstandardになります。
LaunchMode=”singleTop”の場合
LaunchMode=”singleTask”の場合
singleTaskはFLAG_ACTIVITY_NEW_TASKフラグが自動で付加されます。
そのため、SubAを起動した時(右端の列)にタスクが新規作成されています。
LaunchMode=”singleInstance”の場合
singleInstanceはFLAG_ACTIVITY_NEW_TASKフラグが自動で付加されます。
そのため、SubAを起動した時(右端の列)にタスクが新規作成されています。
LaunchModeで注意する事
Context#startActivity( )はNEW_TASKフラグが自動で指定される
Activity#startActivity()とContext#startActivity()を実行して、起動されたActivityでフラグの状態を確認してみます。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) .... findViewById<TextView>(R.id.txt2).setOnClickListener { applicationContext.startActivity(Intent(this, SubActivity__C::class.java)) または this.startActivity(Intent(this, SubActivity__C::class.java)) } }
class SubActivity__C : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) showIntentFlag("Sub_C", this.intent) // フラグの状態を確認 } } fun showIntentFlag(mesg: String, intent: Intent) { val _flags = intent.flags val _buff = StringBuffer("%s[0x%08x]\n".format(mesg, _flags)) if((_flags and Intent.FLAG_ACTIVITY_NEW_TASK) > 0) _buff.append("NEW_TASK\n") if (_flags and Intent.FLAG_ACTIVITY_SINGLE_TOP > 0) _buff.append("SINGLE_TOP\n") if (_flags and Intent.FLAG_ACTIVITY_CLEAR_TOP > 0) _buff.append("CLEAR_TOP\n") // if (_flags and Intent.FLAG_ACTIVITY_NO_HISTORY > 0) _buff.append("NO_HISTORY\n") if (_flags and Intent.FLAG_ACTIVITY_CLEAR_TASK > 0) _buff.append("CLEAR_TASKY\n") if (_flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT > 0) _buff.append("BROUGHT_TO_FRONT\n") if (_flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS > 0) _buff.append("EXCLUDE_FROM_RECENTS\n") if (_flags and Intent.FLAG_ACTIVITY_FORWARD_RESULT > 0) _buff.append("FORWARD_RESULT\n") if (_flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT > 0) _buff.append("LAUNCH_ADJACENTY\n") if (_flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY > 0) _buff.append("LAUNCHED_FROM_HISTORY\n") // if (_flags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK > 0) _buff.append("MULTIPLE_TASK\n") Log.i("ShowIntentFlags", _buff.toString()) }
Activity#startActivity()で起動した場合のフラグ
I/ShowIntentFlags: Sub_C[0x00000000]
Context#startActivity()で起動した場合のフラグ
I/ShowIntentFlags: Sub_C[0x10000000] NEW_TASK
Context#startActivity()で起動した場合にはFLAG_ACTIVITY_NEW_TASKフラグが自動で不可される事が確認できます。
アプリのプロセスが最初にActivityを起動する時、新規にタスクを作成する必要があるためだと思われます。
関連記事: