Android Activity タスクとスタック

アクティビティのタスクとスタックについてまとめます。間違っていたらご指摘のほどよろしくお願いいたします。
## タスク
タスクは複数のアクティビティのまとまりのことです。公式ドキュメント[^2]によると、

> タスクは、ユーザーが新しいタスクを開始したとき、またはホームボタンを使用してホーム画面に移動したときに、「バックグラウンド」に移動できるまとまったユニットです。

です。端末のバックキーの反対側にある □ ボタンを押したら出てくる[最近]画面で、タスクの一覧を確認することができます。

アクティビティAがアクティビティBを起動すると、基本的には、それらのアクティビティはひとつのタスクに所属します。（後述しますが、別のタスクで起動することもできます。）
![activities_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/92a2bbfb-89c0-17fe-98a3-f7a89b15fd33.png)
注意したいのが、タスクは一つのアプリ内のアクティビティをまとめたものとは限らないということです。アプリが異なっても、同じタスクに所属する場合はあります。（むしろデフォルトの起動モードではそうなります）
![activities_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/f39b29c8-5ec6-20ab-c741-f7d8c2614177.png)
### 起動モード
アクティビティは起動モードという属性を設定できます。起動モードによって、どのタスクで起動するのかが変わります。
#### standard
1つのタスクに複数のインスタンスを生成でき、複数のタスクに跨って生成できます。デフォルトの起動モードです。
![activities_launch_mode1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/acf6897c-506e-3849-b746-496a2b1a1a77.png)
#### singleTop
1つのタスクに複数のインスタンスを生成できますが、タスクの一番上に既にある場合は、重ねてインスタンスを作成できません。複数のタスクに跨って生成できます。
![activities_launch_mode2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/9e192145-8dee-3dc5-5ddf-d6ae92933aba.png)
#### singleTask
複数のタスクに跨って存在することはできません。端末内で同じアクティビティのインスタンスを持つタスクは1つだけになります。
![activities_launch_mode3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/217f25dc-112b-f101-f944-830554227898.png)
#### singleInstane
複数のタスクに跨って存在することはできません。端末内で同じアクティビティのインスタンスを持つタスクは1つだけになり、さらに、そのタスク内には他のアクティビティは存在することができません。1タスク内に1つのアクティビティだけある状態になります。
![activities_launch_mode4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/29c20eed-e576-e007-4d55-af4a3751d752.png)
singleTaskとsingleInstanceは、常に自身のタスクで（なければ新しく作成して）、アクティビティを起動したい場合にのみ使用します。

いろいろなアプリから呼び出されてもいい画面だけど、タスクはひとつにしたい、そういうときに使います。例えばブラウザアプリのアクティビティがsingleTaskになっています。いろいろなアプリからブラウザは呼び出されますが、常に自身のタスクで開きます。
![activities_singletask_example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/0b05a63c-033e-4df1-89ac-ae1874ed6ab6.png)
### タスクの確認方法
起動中のタスクはコマンドで確認できます。

```bash
$ adb shell dumpsys activity activities
```

これでもいいですが、アクティビティの詳細情報も出力されて見にくいので、

```bash
$ adb shell dumpsys activity activities | sed -En -e '/Stack #/p' -e '/Running activities/,/Run #0/p'
```

とすると、タスク（とスタック）とタスクに属するアクティビティだけが出力され、見やすくなります。

タスクの状態は[最近]画面でもある程度わかるのですが、必ずしもタスク毎に分かれて表示されるわけではないので、起動したアクティビティが所属してるタスクを正確に把握したい場合は、コマンドで確認するといいと思います。

試しにstandardのアクティビティから別のstandardのアクティビティを起動した時のタスク状態をみてましょう。
（API level 28 のエミュレータで試しました）

```bash
  Stack #106: type=standard mode=fullscreen
    Running activities (most recent first):
      TaskRecord{b418524 #99 A=jp.favolabo.activitystart U=0 StackId=106 sz=2}
        Run #1: ActivityRecord{a2296cf u0 jp.favolabo.activitystart/.StandardTwoActivity t99}
        Run #0: ActivityRecord{fec21ab u0 jp.favolabo.activitystart/.StandardOneActivity t99}
  Stack #0: type=home mode=fullscreen
    Running activities (most recent first):
      TaskRecord{ff86f1 #5 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
        Run #0: ActivityRecord{ef5787f u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t5}
```
`StandardOneActivity`が`StandardTwoActivity`を起動したんですね。`NexusLauncherActivity`というのはホーム画面のアクティビティになります。
#### IntentにフラグをつけてActivityを起動
`FLAG_ACTIVITY_NEW_TASK `というフラグがあります。実は、アクティビティの起動モードをsingleTaskにすると、このフラグが設定されて起動されています。コードで書くと以下のようになります。

```kotlin:Activity.kt
val intent = Intent(this, StandardActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
```

なので動的にstandardのアクティビティをsingleTaskのように起動したい場合があれば、上記のように設定すればよいかと思います。

ちなみにですが、さきほどのタスクを確認するコマンドで、アクティビティの詳細情報まで出力して

- standardのアクティビティに`FLAG_ACTIVITY_NEW_TASK`をつけて起動した場合
- singleTaskのアクティビティを普通に起動した場合

を見比べてみると、

```:standardのアクティビティに`FLAG_ACTIVITY_NEW_TASK`をつけて起動した場合
Intent { flg=0x10000000 cmp=jp.favolabo.activitystart/.StandardActivity }
```

```:singleTaskのアクティビティを普通に起動した場合
Intent { flg=0x10000000 cmp=jp.favolabo.activitystart/.SingleTaskActivity }
```

とIntentの情報があり、`flg`が同じ(=`FLAG_ACTIVITY_NEW_TASK`)であることがわかります。
### taskAffinity
singleTaskのアクティビティを起動するときに、どのタスクで起動すべきかを決めるための属性として、taskAffinity（親和性）というものがあります。

ためしに、同じアプリでsingleTaskのアクティビティをstandardのアクティビティから起動してみます。すると、同じタスク上で起動します。

これはなぜかというと、taskAffinityが同じアクティビティは、同じタスク内に起動しようとするからです。
taskAffinityはデフォルトではパッケージ名になります。たとえば `com.hoge.fuga.app` というパッケージ名だと、そのアプリ内のアクティビティのtaskAffinityも `com.hoge.fuga.app` になります。
![activities_taskaffinity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/2530ab05-0cc0-ec34-f2cb-2a39066b089d.png)

必ず別タスクでsingleTaskのアクティビティを起動したい場合は、taskAffinityを明示的に指定します。

```xml:AndroidManifest.xml
<activity android:name=".SingleTaskActivity"
    android:launchMode="singleTask"
    android:taskAffinity="app.hoge.fuga2"/>
```

どのタスクでアクティビティが起動するか、こちら[^3] の解説記事でわかりやすく、起動モードごとにアニメーション付きで説明されていますので、是非見てみてください。
## スタック
アクティビティを起動すると、後入れ先出しのいれもの（スタック）に積まれます。バックボタンを押すと、現在表示中のアクティビティから順に取り出されて、ホーム画面が表示されるまで、一つ前のアクティビティに戻っていきます。

例えばsingleTaskアクティビティであるブラウザを呼び出した場合は、以下のようなスタックになります。

```bash
  Stack #120: type=standard mode=fullscreen
    Running activities (most recent first):
      TaskRecord{45197fa #113 A=com.android.chrome U=0 StackId=120 sz=1}
        Run #0: ActivityRecord{976c635 u0 com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity t113}
  Stack #119: type=standard mode=fullscreen
    Running activities (most recent first):
      TaskRecord{86811ab #112 A=jp.favolabo.activitystart U=0 StackId=119 sz=1}
        Run #0: ActivityRecord{847e274 u0 jp.favolabo.activitystart/.StandardActivity t112}
  Stack #0: type=home mode=fullscreen
    Running activities (most recent first):
      TaskRecord{ff86f1 #5 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
        Run #0: ActivityRecord{ef5787f u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t5}
```
ブラウザでバックボタンを押していくと、
ブラウザ（`ChromeTabbedActivity`） → `StandardActivity` → ホーム画面（`NexusLauncherActivity`）
の順で取り出されていきます。
![activities_pop1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/3c97cf5f-30a1-444f-b69b-742978fc0912.png)

ここで、試しにブラウザが表示されたら、ホームボタンを押して、呼び出し元のアクティビティ（`StandardActivity`)をアイコンからタップして起動してみましょう。するとスタックは以下のように変わりました。

```bash
  Stack #119: type=standard mode=fullscreen
    Running activities (most recent first):
      TaskRecord{86811ab #112 A=jp.favolabo.activitystart U=0 StackId=119 sz=1}
        Run #0: ActivityRecord{847e274 u0 jp.favolabo.activitystart/.StandardActivity t112}
  Stack #0: type=home mode=fullscreen
    Running activities (most recent first):
      TaskRecord{ff86f1 #5 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
        Run #0: ActivityRecord{ef5787f u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t5}
  Stack #120: type=standard mode=fullscreen
    Running activities (most recent first):
      TaskRecord{45197fa #113 A=com.android.chrome U=0 StackId=120 sz=1}
        Run #0: ActivityRecord{976c635 u0 com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity t113}
```

スタックの一番上が`StandardActivity`になっていて、次にホーム画面(`NexusLauncherActivity`)になっていますね。なので、当たり前ですが、バックボタンを押すと、ホーム画面に戻ります。
![activities_pop2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247791/1a83d9ed-6900-9e51-2ad8-177deb80bbb2.png)
## 参考
[親和性と新しいタスク](https://developer.android.com/guide/topics/fundamentals.html?hl=ja#afftask)
[アクティビティのリファレンス](https://developer.android.com/guide/topics/manifest/activity-element?hl=ja)
[^1]: [アクティビティの概要](https://developer.android.com/guide/components/activities/intro-activities?hl=ja)
[^2]: [タスクとバックスタックについて](https://developer.android.com/guide/components/activities/tasks-and-back-stack)
[^3]: [起動モードのチートシート](https://itnext.io/the-android-launchmode-animated-cheatsheet-6657e5dd9b0f)