woshidan's blog

あいとゆうきとITと、とっておきの話。

インテントフラグとActivityStackのふるまいについていくらか

AndroidActivity のスタックの振る舞いを設定するものとして、

  • AndroidManifest.xml に記述する activity 要素の launchMode 属性
  • Activity を立ち上げる時に使う Intent に付与するインテントフラグ

があります。先日、launchModeの話をしたので、今日はインテントフラグの実験を少ししようかと思います。

今回インテントフラグがどんなものか確認する際に取り上げる具体例としては、

  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_NO_HISTORY

あたりにします。今回の実験に使ったコードの全体像はこちらで適宜、/* some flags */ 周辺のコードを編集した分を実行結果と一緒に紹介します。

TL;DR

長くなったので。

  • FLAG_ACTIVITY_SINGLE_TOP
    • launchModesingleTop と同じ
    • このフラグをつけて起動したActivityと同じActivityがスタックの一番上にあるなら、既存のインスタンスonNewIntent()を呼びだす
  • FLAG_ACTIVITY_CLEAR_TOP
    • このフラグをつけて起動した Activity よりスタックの上にある Activity を破棄して、フラグをつけた Activity を呼び出す
    • FLAG_ACTIVITY_SINGLE_TOP との併用している、または、起動した ActivitylaunchModesingleTop の場合は既存のインスタンスが再利用される
    • そうでない場合は、既存の Activityインスタンスを破棄してから新しい Activityインスタンスを生成して起動する
  • FLAG_ACTIVITY_NO_HISTORY
    • このフラグをつけて起動した ActivityActivity のスタックに積まれない
    • = onActivityResult が決して呼び出されない

FLAG_ACTIVITY_SINGLE_TOP

まず、MainActivity を起動するときだけ、FLAG_ACTIVITY_SINGLE_TOP を利用してどのような挙動をするか見てみましょう。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

MainActivityが一番上にあるときはその上にActivityが積まれず

NextActivityが一番上にあるときは新しいMainActivityが積まれます。

launchModesingleTop と同じ振る舞いですね。

FLAG_ACTIVITY_CLEAR_TOP

MainActivityを表示しようとしたとき、FLAG_ACTIVITY_CLEAR_TOPインテントフラグを付与することにします。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

MainActivityが一番上にあるとき、すでにあるActivityが破棄されてから別のActivityが作り直されて一番上に積まれ直します。

Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());インスタンスが変わっているというログがこちらです。

D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@3a2375cd
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@21a00ad7
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@ac67751

何枚か NextActivity をスタックに積んでから MainActivity を起動し、その後バックキーを押すとアプリが終了します。

この場合のLog.d("MainActivity", "MainActivity " + MainActivity.this.toString());インスタンスが変わっているというログがこちらです。

D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@2007c35
D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@25e3619b

もう少し別の条件で検証するとわかりやすいですが、起動しようとした MainActivity より上のスタックにある NextActivity と古い MainActivityを捨て、MainActivity を作り直して、呼び出していることがわかります。

ドキュメントのフラグ一覧のところの FLAG_ACTIVITY_CLEAR_TOP の欄には、

If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.

とあり、これは上記の挙動と異なるように見えます。

しかし、これは仕様どおりで、さらに FLAG_ACTIVITY_CLEAR_TOP単体の項目のドキュメントを読むと、FLAG_ACTIVITY_SINGLE_TOP との併用か、 ActivitylaunchModesingleTop に設定されていなければ、既存のインスタンスへちょっとIntentが送られないことがあることがわかります。

それでは、 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_SINGLE_TOP との併用した場合の挙動を

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d("MainActivity", "onNewIntent");
        Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show();
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
}

で試してみましょう。今度は、一覧の部分にあるように既存のMainActivityのインスタンスにIntentが送られるようになったみたいです。

FLAG_ACTIVITY_NO_HISTORY

どのActivityを起動するときも FLAG_ACTIVITY_NO_HISTORYインテントフラグを付与してがんがんActivityを起動してみます。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });
    }
}

たくさんスタックを積んだはずが、2回バックキーを押すとアプリが終了してしまいました。

最初の一回のバックキーで一番最初に起動した MainActivity へ戻ってしまったようです。

FLAG_ACTIVITY_NO_HISTORYインテントフラグをつけて起動されたActivityは、Activityのスタックに積まれないようです。

インテントフラグはActivityを起動する側が指定するので、使う側の都合によってActivityのスタック上での扱いを決める方法、みたいな感じなんですかね。

現場からは以上です。

参考

Intent | Android Developers