launchModeがsingleTask
やsingleInstance
であるActivityのインスタンスを、完全なsingletonのつもりで設計していたら、えらくハマったのでメモまで。
要は、singleTask / singleInstanceなActivityでfinish()
メソッドを呼んだ後に、自分自身を再起動すると、別々のインスタンスで共存する区間ができてしまう。
(おさらい1) 通常のActivity遷移
Activity A からActivity B をstartActivity()
で起動する場合。
ActivityのlaunchModeはstandard
であるとしておく。
このとき、Activityのライフサイクルにまつわるメソッドは以下のような順序で呼ばれる。
- A から B をstartActivity()
- A: onPause() ※1
- B: onCreate()
- B: onStart()
- B: onResume()
- A: onStop() ※2
ポイントは2箇所。
- ※1 起動元 A の
onPause()
処理が終わらないと、起動先の B のonCreate()
は呼ばれない - ※2 起動元 A の
onStop()
やonDestroy()
は、起動先の B のonResume()
の後に呼ばれる模様。
ただし、A のonStop()やonDestroy() は状況によってはそもそも呼ばれないことがある。(Aの画面が隠れず見えている場合など)
※1については、気にしている人が少ないようで、onPause()で重い処理を書いてしまうと、画面遷移における速度低下の原因になってしまう。
(おさらい2) singleTask / singleInstance な Activityの再起動
launchModeがsingleTask
もしくはsingleInstance
であるActivity A について、A から A を起動(再起動)する。
「同じActivityに再起動させることなんてあるのか」と突っ込まれそうだが、起動引数に応じて画面の表示内容を更新する場合などに利用できる。
このとき、Activityのライフサイクルにまつわるメソッドは以下のような順序で呼ばれる。
- A から A をstartActivity()
- A: onPause()
- A: onNewIntent()
- B: onResume()
ポイントは、singleTask
やsingleInstance
の場合は、起動元と同じActivityインスタンスが呼び出される。
そのため、onCreate()
は呼ばれず、代わりに onNewIntent()
で再起動時に指定するIntentが渡される。
(本題) 別々のActivityインスタンスが共存するケース
上記の (おさらい2) とほとんど同じだが、singleTask
もしくはsingleInstance
の Activity A について、 A から A を起動 (再起動) する。 ただし、再起動前に、自分自身を終了させるため finish()
メソッドを実行する。
例えば、以下のような感じ。
: (略)
((Button) findViewById(R.id.button))
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 一旦、自分自身を終了してから、
finish();
// 続けて、自分自身を起動する。
Intent i = new Intent(MainActivity.this, MainActivity.class);
startActivity(i);
}
});
: (略)
このとき、Activityのライフサイクルにまつわるメソッドは以下のような順序で呼ばれる。
- Aをfinish()し、続けて A から A をstartActivity()
- A: onPause()
- A': onCreate() -- (1)
- A': onStart()
- A': onResume()
- A: onStop()
- A: onDestroy() -- (2)
ここで A' はActivity A とは別に生成されるActivityインスタンス。
上記のとおり、(1) から (2) の間は、singleTask
/ singleInstance
であるActivityのインスタンスが2個共存することになり、こうしたActivityのインスタンスはVM上で完全なsingletonではないことが分かる。
共存区間(1)-(2)は刹那なため、実影響は少ないように思えるが、自身のハマりケースの場合、Activityとは別のアプリコンポーネントの状態制御をonPause()やonDestroy()などのメソッドをトリガーに行うよう設計していて(設計されていて)、どハマりした。上記のように、finish()
される A インスタンスの onDestroy() が最後っ屁のように実行されてしまうため、意図しない終了処理が実行されてしまう。
また余談だが、finish()
メソッドもかなりのクセもの。
Activityの onDestory()
が確実にCallされる目的とかで、開発終盤のバグFixフェイズにしれっとまぎれてくることがあるが、Activityのライフサイクルの見通しを悪くしてしまうので、多用を避けることをお勧めしたい。