読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Android アプリ開発独習メモ #4 — Activity のライフタイムの管理

Learning Android アプリ

今回は #2 の内容の続きで、“Getting Started” の中の “Managing the Activity Lifecycle” の話。

ユーザーがアプリを起動したり、別のアプリに行ったり、またアプリに戻ってきたり、ということをしたとき、アプリの Activity インスタンスはそのライフサイクルの中の別の状態に遷移する。 状態の遷移時には、Android のシステムによって Activity に定義されたコールバックメソッドが呼び出されるので、そこで必要な処理を行うことができる。

この記事では、Activity のライフサイクルにおける状態と、その遷移時のコールバックメソッドについて述べる。 元の資料ではデモ用のプロジェクトも公開されているので、そちらを見ながら進めるとわかりやすいだろう。

Activity の開始

プログラムが開始されたときに main 関数が実行される、というようなプログラミングのパラダイムとは違い、Android システムによってライフサイクルの状態変化に応じたコールバックメソッドが呼び出されることで Activity インスタンスの処理は開始される。

ライフサイクル中のコールバックを理解する

Activity のライフサイクル中の状態とその変化時に呼び出されるコールバックメソッドは資料中の図を見るとわかりやすい。 ホーム画面でアプリのアイコンがタッチされてアプリの起動シーケンスが開始されると、まず onCreate が呼び出されてから Created 状態になり、すぐさま onStart メソッドが呼び出されて Started 状態になり、そのあとすぐ onResume メソッドが呼び出されて Resumed 状態になる。 Resumed 状態がアプリが最前面で動いている状態である。

コールバックメソッドは必ずしも実装する必要はないが、下記のようなことが起こらないようにコールバックメソッドを正しく実装する必要がある。

  • アプリ実行中に電話の呼び出しがあったり別のアプリへの遷移があってもクラッシュしないように。
  • アプリをアクティブ状態で使っていない場合にシステムリソースを浪費しないように。
  • アプリ実行中に別のアプリなどが起動され、その後またアプリにユーザーが戻ってきた場合に、ユーザーが処理中だったものが失われないように。
  • 画面が回転されたときに、クラッシュしたりユーザーが処理中だったものが失われないように。

資料中の図を見ればわかるように多くの状態があるが、Created 状態などすぐ次の状態に移るような状態もある。 静的な状態は次の 3 つ。

  • Resumed : この状態の Activity は最前面で動いていて、ユーザーはこの Activity とやりとりできる。 (この状態は “running” 状態と呼ばれることもある。)
  • Paused : 半透明だったり画面全体を覆わないような Activity が最前面で動いている場合に、その Activity に一部分が隠された Activity はこの状態になる。 この状態の Activity はユーザー入力を受け取ることも任意のコード (?) を実行することもできない。
  • Stopped : この状態の Acticity は完全に隠れていて、ユーザーからは見えない――すなわち、バックグラウンドにあるとみなすことができる。 この状態にいる間、Activity のインスタンスとそのすべての状態情報 (メンバ変数など) は保たれるが、任意のコード (?) は実行されえない。

アプリの Launcher Activity を定める

ホーム画面のアプリのアイコンがクリックされたときに開始される Activity は、“Launcher” Activity (あるいは “Main” Activity) として定められたもの。 どの Activity が Launcher Activity であるかは、AndroidManifest.xml で指定される。

Launcher Activity を manifest ファイルで指定するには、MAIN アクションと Launcher カテゴリを含んだ intent-filter 要素を使う。

Android SDK ツールで生成されたプロジェクトだと、最初からこの記述がなされている。 なお、この intent filter がない Android アプリは、ホーム画面にアイコンが表示されない。

新しい Activity インスタンスの生成

Activity の新しいインスタンスは、Android システムによって onCreate メソッドが呼び出されて生成される。

onCreate メソッドには、ライフサイクル開始時に一度だけ実行されるべきアプリケーションの基本的な開始処理を記述する。 例えば、UI 定義や、場合によってはクラススコープ変数のインスタンス化などを行う。 onCreate メソッドの処理が終了した後、システムはすぐさま onStart メソッド呼び出しと、さらにそのあと onResume メソッド呼び出しを続ける。 Created 状態と Started 状態にとどまることはない。 技術的には、onStart メソッドが呼び出されたときにはアプリは可視状態になっている。

Activity の破棄

Activity のライフサイクルの最初に呼び出されるのが onCreate メソッドであるが、一方で最後に呼び出されるのは onDestroy メソッドである。

onPause メソッドや onStop メソッドでクリーンナップ処理を行っているはずなので、このメソッドを実装する必要は基本的にはないはずである。 しかし、onCreate メソッドでバックグラウンドスレッドを生成した場合や、他の長時間動かされるリソースが解放されずに残る場合などは、このメソッドで解放する必要がある。

唯一の例外を除き、onDestroy メソッドは onPause メソッドや onStop メソッドが呼び出された後に呼び出される。 唯一の例外は、onCreate メソッド内で finish メソッド が呼び出された場合である。

Activity の一時停止と再開 (Pausing and Resuming)

ダイアログ形式の半透明の Activity を最前面に出した場合など、別の Activity によって Activity の一部が隠されることがある。 そのような状態 (Paused 状態) になるとき、Activity の onPaused メソッド が呼び出される。 また、Activity がユーザーから完全に見えなくなるようになるときにも、最初に Paused 状態に遷移するので、onPaused メソッドが呼び出される (その場合は、Paused 状態になった後すぐに Stopped 状態に遷移する)。

Paused 状態の Activity が最前面に復帰するときには、onResume メソッドが呼び出される。

Pause

onPause メソッドでは次のようなことをすべき。

  • アニメーションの停止や、CPU を消費する他の進行中の処理。
  • 保存されていない変更の保存; ただし、アプリから離れるときにユーザーが永続的に保持されるものだと期待する場合にのみ (電子メールの下書きなど)。
  • センサー (GPS など) のハンドラや broadcast receivers などのシステムリソース、バッテリーに影響を与えるもののうち、Pause 中はユーザーが必要としないようなその他のリソースの解放。

一般的には、ユーザーによる変更は onPause メソッドでは永続的なストレージに保存すべきではない。 変更が自動的に保存されることをユーザーが期待していると確信できる場合にのみするべきである。 しかしながら、次の Activity への目に見える遷移が遅くなってしまうため、DB への書き込みなどの CPU に負荷がかかるような処理を onPause メソッドで行うことは避けるべきである (代わりに onStop メソッドの中で行うべき)。

Resume

Paused 状態から Resumed 状態に復帰する場合は、onResume メソッドが呼び出される。 onResume メソッドは Pause 状態から Resumed 状態への遷移時だけでなく、Started 状態から Resumed 状態への遷移時にも呼び出されることに注意。

Activity の停止と再起動 (Stopping and Restarting)

アプリがずっと健在で処理途中のものが失われていないとユーザーに感じさせるために、停止と再起動の処理を適切に実装することは重要である。 アプリの停止と再起動が行われるシナリオとしては次のようなものがある。

  • ユーザーが最近のアプリのウィンドウを開いて、別のアプリに移る。 最初に最前面にあった Activity は停止する。 ユーザーがホームアイコンをタップするなどして最初のアプリに戻ったとき、Activity は再起動する。
  • ユーザーがアプリ内でアクションを起こして、別の Activity を開始する。 最初の Activity は停止する。 新しく開始された Activity でユーザーが戻るボタンを押したら、最初の Activity は再起動する。
  • アプリ使用中に電話を受け取る。

停止と再起動のためのコールバックメソッドとして次のものがある。

Stopped 状態でもメモリ上に保持されているので、単純な Activity の場合はこれらのメソッドを実装しなくても問題ない。

Stop

onStop メソッドが呼び出されるとき、Activity の UI はもはや表示されていないということが保証されているので、ほとんどすべてのリソースは解放できるはずである。 Stopped 状態の Activity はシステムによって破棄される可能性がある (極端な場合は onDestroy メソッドの呼び出しがなされない) ので、メモリリークしないように onStop メソッドでリソースの開放をしておくことが重要である。

Stopped 状態の Activity はメモリ上に保持されて、Activity が再開されたときにはそれが使われる。 よって、コンポーネントの最初期化は必要ないようになっている。 View オブジェクトも保持されるので、例えば EditText にユーザーが入力した文字列なども保持される。 そういったものの保存とリストアの処理は書かなくてよい。

また、たとえ Stopped 状態の Activity をシステムが破棄したとしても、View オブジェクトの状態は Bundle (a blob of key-value pairs) に保持されていて、その Activity の同じインスタンス (破棄されたのに同じインスタンス??) に戻ってきたときには状態が復元されるようになっている。 このことについての詳細は次のレッスンで。

Start / Restart

Stopped 状態の Activity が最前面に戻されたとき、onRestart メソッド と onStart メソッドが呼び出される。 通常は onStart メソッドの処理だけで事足りると思われるが、Stopped 状態からの復帰の場合にのみ行うべき処理がある場合は onRestart メソッドに記述する。

onRestart メソッドを必要とすることは一般的ではないので、このメソッドに関するガイドラインはない。 (通常は onStop メソッドに対応するものとして onStart メソッドを使用すればよい。)

Activity の再生成

ユーザーが戻るボタンを押したり、Activity 自身が自分を終了させた (finish メソッドで?) 場合、システムの方針としては Activity のインスタンスは永遠に使われないものとする。 なぜなら、その引き金は Activity をもう必要としないことを示すものであるからである。 しかしながら、システムの制約によって Activity が破棄された場合、実際の Activity インスタンスは失われるが、システムはそのように終了させられたことを覚えていて、ユーザーがそこに戻ってきた場合には、破棄される前に保存していたデータを使って新しい Activity インスタンスを生成する。 システムが前の状態をリストアするために使用する保存されたデータは 「インスタンス状態」 と呼ばれるもので、それは Bundle オブジェクトに保持された key-value ペアの集まりである。

ちなみにスクリーンが回転させられた時も Activity は一度破棄されて再度生成されるので注意が必要である。

デフォルトでは、Activity のレイアウト中の View オブジェクトの状態が Bundle オブジェクトに保持されるようになっている。 なお、View の状態をリストアするために、それぞれの View オブジェクトは android:id 属性 によって指定される一意の ID を持っていなければならない。

Activity の状態を保存する

デフォルトで保存される情報以外も Bundle に保存したい場合は、onSaveInstanceState メソッドをオーバーライドすること。

スーパークラスのメソッド呼び出しを忘れるとデフォルトで保存されるものが保存されないので、スーパークラスのメソッド呼び出しを忘れないように。

Activity の状態を復元する

破棄された Activity が再度生成された場合、Bundle オブジェクトから状態を復元できる。 onCreate メソッドと onRestoreInstanceState メソッドの 2 つのコールバックメソッドの引数として Bundle オブジェクトが渡される。

onCreate メソッドは、再生成時だけでなく通常生成時にも呼び出されるわけだが、そのときには引数は null になっているので注意が必要。 onRestoreInstanceState メソッドの方は Bundle オブジェクトがある場合にのみ呼び出されるので、null かどうかの確認は必要ない。

こちらもスーパークラスのメソッド呼び出しを忘れないように。 実行時のリスタートイベントによる Activity の再生成 (スクリーンの回転も) に関しては次を見ること。

まとめ

  • ホーム画面のアプリのアイコンがクリックされたときに実行される Activity は manifest の記述で決定される。
  • Activity は生成されてから破棄されるまで、ライフタイム中の複数の状態を遷移しうる。
  • Activity のライフタイム中の状態遷移時にはコールバックメソッドが呼び出される。
  • コールバックメソッドでは適切にリソースの解放などをしなければならない。
  • Activity が再生成されるときにはインスタンス状態を Bundle オブジェクトに保持しておける (?)