アゲブログ

プログラマーです。

Activityのライフサイクル、バックスタック、タスク

あまり理解できていないのでまとめます。

Activityとは?

AndroidにおけるActivityとは、ユーザーがボタンをタップしたり文字を入力したりする画面そのものです。AndroidアプリはActivityからActivityを起動したりすることで画面遷移のような動作を実現することができます。

ライフサイクルとは?

MainActivityからSubActivityを起動した場合、SubActivityが生成され画面に表示されます。このとき、MainActivityは画面には表示されていないものの裏側では保持されている状態です。そして、SubActivityでバックボタンをタップした際はMainActivityが画面に表示(再開)され、SubActivityが破棄されます。このようなActivityの作成・停止・再開・破棄のような推移をActivityのライフサイクルといいます。開発者は状態が変化した際に実行されるコールバックメソッド(ライフサイクルメソッド)に処理を記述していくことになります。下図はActivityのライフサイクルと主なコールバックメソッドです。

activity_lifecycle.png (45.7 kB)

https://developer.android.com/reference/android/app/Activity

Android OSはメモリ不足などにより非表示状態にあるアプリのプロセスを強制終了することがあるため、すべてのライフサイクルメソッドが必ず呼び出されるという保証はありません。

onCreate()

Activityが作成されるタイミングで呼び出されます。Activityのレイアウト設定、ボタンなどのViewにリスナーを登録するような初期化処理を行います。なお、onCreate()メソッドは必ず実装しなければなりません。

onRestart()

Activityが再開される直前に呼び出されます。

onStart()

Activityが画面に表示される直前に呼び出されます。

onResume()

Activityがフォアグラウンド(ユーザーが操作可能な状態)になる直前に呼び出されます。

onPause()

Activityがバックグラウンドに移行する直前に呼び出されます。たとえば、SubActivityがMainActivityから起動された場合にMainActivityのonPause()メソッドが呼び出されます。このコールバックメソッドが呼び出されたあとはOSによって強制終了される可能性があるため、重要なデータの保存などはここで行うことが推奨されているようです。さらに、このメソッドの処理が完了しないと別のActivityを開始できないため軽い処理にしなければなりません。

onStop()

Activityが非表示になったときに呼び出されます。

onDestroy()

Activityが破棄されるときに呼び出されます。

実際に動かしてみる

Activityのそれぞれのライフサイクルメソッドにログを出力する処理を仕込み、どのようなタイミングでメソッドが呼び出されているのかを確認 します*1 。 重複するログは省略します。

MainActivity.kt

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {

    private val tag = MainActivity::class.java.simpleName

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(tag, "onCreate()")
    }

    override fun onRestart() {
        super.onRestart()
        Log.d(tag, "onRestart()")
    }

    override fun onStart() {
        super.onStart()
        Log.d(tag, "onStart()")
    }

    override fun onResume() {
        super.onResume()
        Log.d(tag, "onResume()")
    }

    override fun onPause() {
        super.onPause()
        Log.d(tag, "onPause()")
    }

    override fun onStop() {
        super.onStop()
        Log.d(tag, "onStop()")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(tag, "onDestroy()")
    }
}

アプリケーションを起動 → バックボタンをタップ

Image from Gyazo

アプリケーションを起動すると起点となるActivityが生成されて画面に表示されます。

09-16 00:43:00.606 7206-7206/org.ageage.myapplication D/MainActivity: onCreate()
09-16 00:43:00.608 7206-7206/org.ageage.myapplication D/MainActivity: onStart()
09-16 00:43:00.610 7206-7206/org.ageage.myapplication D/MainActivity: onResume()

バックボタンをタップするとActivityが破棄されるためonDestroy()が呼び出されるようです。

09-16 00:43:01.568 7206-7206/org.ageage.myapplication D/MainActivity: onPause()
09-16 00:43:01.928 7206-7206/org.ageage.myapplication D/MainActivity: onStop()
09-16 00:43:01.929 7206-7206/org.ageage.myapplication D/MainActivity: onDestroy()

アプリケーションを起動 → ホームボタンをタップ

Image from Gyazo

バックボタンをタップした際とアプリケーションは同じような動作をしているように見えますが ホームボタンをタップした場合はonDestroy()が呼び出されていません。

09-16 00:45:12.381 7586-7586/org.ageage.myapplication D/MainActivity: onPause()
09-16 00:45:12.411 7586-7586/org.ageage.myapplication D/MainActivity: onStop()

これはAndroidがActivityを管理する方法に起因するものです。

Activityはスタック形式のバックスタックに保存され、複数のActivityが積まれている状態でバックボタンをタップするとフォアグラウンド(スタックの先頭)のActivityが破棄されて以前のActivityが再開するようになっています。

diagram_backstack.png (22.6 kB)

https://developer.android.com/guide/components/activities/tasks-and-back-stack

また、連続して開始されたすべてのActivityはタスクという単位で管理されます。たとえば、2つのActivityが積まれているフォーカスされた(画面に表示されている)タスクAが存在する状態で、ホームボタンをタップするとタスクAはバックグラウンドに移動します。この状態になってもタスクAは保存されているためActivityは破棄されません。そして、ホーム画面から別のアプリケーションを起動すると新たにタスクBが作られ(マルチタスク)、タスクAのバックスタックとは別にタスクB用のバックスタックでActivityが保存されます。このような構造になっているため、ユーザーは複数のアプリケーションを切り替えながら操作することができます。

diagram_multitasking.png (12.1 kB)

https://developer.android.com/guide/components/activities/tasks-and-back-stack

そのため、ホームボタンをタップしてもActivityが破棄されずにバックグラウンド(非表示になる)に移動するためonDestroy()が呼び出されず、バックボタンをタップした場合はバックスタックからActivityがPop(破棄)されるためonDestroy()が呼び出されるのです。

アプリケーションを起動 → ホームボタンをタップ → 起動したアプリケーションを選択

Image from Gyazo

一度起動したアプリケーションをホーム画面から選択した場合は、すでにタスクが作られているためActivityを再開した際に呼び出されるonRestart()が呼び出されています。マルチタスクボタンをタップしたあとにタスクを選択した場合も同様です。

09-16 02:27:08.769 11765-11765/org.ageage.myapplication D/MainActivity: onRestart()
09-16 02:27:08.770 11765-11765/org.ageage.myapplication D/MainActivity: onStart()
09-16 02:27:08.772 11765-11765/org.ageage.myapplication D/MainActivity: onResume()

アプリケーションを起動 → マルチタスクボタンをタップ → タスクを削除

Image from Gyazo

マルチタスクボタンがタップされた際はホームボタンがタップされた場合と同じようです。

09-16 03:00:34.158 13627-13627/org.ageage.myapplication D/MainActivity: onPause()
09-16 03:00:34.169 13627-13627/org.ageage.myapplication D/MainActivity: onStop()

タスクを削除するとActivityが破棄されるのでonDestroy()が呼び出されます。

09-16 03:00:35.617 13627-13627/org.ageage.myapplication D/MainActivity: onDestroy()

アプリケーションを起動 → 画面を回転

Image from Gyazo

画面を回転した場合は画面構成が変更されるためActivityは破棄されてから再生成されます。

09-16 03:15:13.534 14217-14217/org.ageage.myapplication D/MainActivity: onPause()
09-16 03:15:13.541 14217-14217/org.ageage.myapplication D/MainActivity: onStop()
09-16 03:15:13.542 14217-14217/org.ageage.myapplication D/MainActivity: onDestroy()
09-16 03:15:13.593 14217-14217/org.ageage.myapplication D/MainActivity: onCreate()
09-16 03:15:13.594 14217-14217/org.ageage.myapplication D/MainActivity: onStart()
09-16 03:15:13.597 14217-14217/org.ageage.myapplication D/MainActivity: onResume()

このとき、Activityの状態保持には注意が必要です。たとえばボタンをタップした数を画面に表示するアプリケーションがあるとします。

Image from Gyazo

ある程度ボタンをタップして画面に表示されている数字が大きくなった状態で画面を回転すると、数字は0になってしまいます。

Image from Gyazo

これはActivityが再生成される際に数字の保存と復元を行っておらず、初期値の0になってしまっているためです。画面状態の保存にはonSaveInstanceState()コールバックを使用します。onSavedInstanceState()に引数としてBundle型のインスタンスが渡されます。Bundleはアプリの状態をKey-Value形式で管理するためのクラスです。保存されたBundleはActivityの再作成時にonCreate()の引数として渡されます*2

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textView = findViewById<TextView>(R.id.textView)
        textView.text = savedInstanceState?.getString(KEY_TAPPED_NUM_TEXT) ?: "0"

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            val tappedNum = Integer.valueOf(textView.text.toString())
            textView.text = "${tappedNum + 1}"
        }
    }

    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)

        val textView = findViewById<TextView>(R.id.textView)
        outState?.putString(KEY_TAPPED_NUM_TEXT, textView.text.toString())
    }

    companion object {
        const val KEY_TAPPED_NUM_TEXT = "tapped_num_text"
    }
}

上記はonSavedInstanceState()で画面に表示されている数字を文字列として保存しています。そして、onCreate()の引数のBundle型のインスタンスnullでなければ(onSavedInstanceState()で値が保存されている場合)数字を取得して設定し、nullであれば初期値として0 を設定しています。こんな感じで画面が回転した際の処理はちょっと面倒臭いっすね。

他にも縦画面固定にしたり、画面を回転した際にActivityが破棄されないように設定するなどの対策を挙げることができますが、それぞれに利点・欠点があります。

最後に

Androidにおいては基礎的?な部分だと思うのですが、調べれば調べるほど深みにハマってしまうような感じです。Fragment編も書きたいと思います。。

あと、文章を書くのが苦手なので克服していきたい。。構成とか。。

参考

*1:launchMode属性はstandard

*2:onRestoreInstanceStateコールバックの引数にも保存されたBundleが同じように渡されますが、今回はonCreateコールバックを使用します