Android

【Android】savedInstanceStateの意味と開発者オプション【初心者向け】

More than 1 year has passed since last update.

はじめに。

savedInstanceStateとかって何?何なの?普通に気にせず開発してても何も問題ないんだけど?
何?無視してもいいの?
え?画面回転するときにActivity破棄される?その時使われる?じゃあ回転させなかったらいいの?
っていう人向け。

パズドラとかGoogleMapとか他のアプリ開いてしばらくいじったあと戻ってきたらアプリがなんか変!またはアプリが落ちた!
こんなことがあなたの作ってるアプリに起きるかもしれません。

Activityは死にます。
プロセスも死にます。
なのでたまに開発者オプション「アクティビティを保持しない」「バックグラウンドプロセスの上限」を使うと良いよって話です。

savedInstanceState系の理解の出発点になれれば。


ActivityのonCreateの引数 Bundle savedInstanceState さん

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

これって何?無視して開発してるけど、問題ないんだけど…?
Androidを始めたばかりのころ、こう思っていました。

この引数、普段はnullなんですが、特定の場合に値が入ってきます。
特定の場合 ―― 大雑把にいうと、Activityが作り直された時です。

アクティビティ起動する→閉じる→起動する

これもある意味作り直してるのですが、ここでいう作り直すは少し違います。

Activityの作り直し(以下、復元と呼ぶ)、大きく言うと2つあります。

1.構成の変更時

言語設定を切り替えた時など、特定の場合にActivityが再起動します。
参考:アクティビティの再作成 | Android Developers

Activityが再作成される構成変更の一覧はこちら(Android Developers)に定義されています。

具体例

  • 設定 -> 言語と入力 -> 言語 から言語を切り替えた時
  • 設定 -> ディスプレイ -> フォントサイズ からフォントサイズを切り替えた時
  • 画面回転時

Activityが破棄→復元されると背景が赤黒くなる痛ましいサンプルアプリを作りました。ソースはこちら

画面回転

起動して 回転すると死にます
20170228-161108.png 20170228-161113.png

言語設定

起動して 言語設定に行って 言語を切り替えて 戻ってくると 破棄されています
20170228-170631.png 20170228-170711.png 20170228-170726.png 20170228-170734.png 20170228-170743.png

2.メモリ不足とかでプロセスごとkillされた時

AndroidにはLow Memory Killerという仕組みが備わっており、フォアグラウンドのアプリでメモリが足りなくなった場合に、バックグラウンドに居る要らないアプリ(プロセス)をバッタバッタと切り捨てていきます。
こうして、あなたのアプリも切られ対象になると、プロセスごとkillされ、Activityも破棄されてしまいます。
その後、ホームのアイコンなどから当該アプリ起動しようとすると、別プロセスで新しく起動するとともに、破棄されたActivityは復元されます。

起動して いろんなアプリ起動してしばらくして戻ってくると 破棄されていることがあります
20170228-171555.png 20170228-172137.png 20170228-172151.png

いかがでしょうか。
要は、システム的な都合で一旦ActivityをDestroyしなきゃいけないような時に、Activityの作り直し(破棄→復元)が行われます。
自分でバックキー押していつも通りActivityを閉じるなどの場合は関係のない話です。

ちょっとしたアプリを作る場合、開発中に出くわすのは、せいぜい画面の回転くらいではないでしょうか。画面を縦固定にしている人は、出くわすことすらないかもしれませんね。
でも、リリースしたあとのアプリは、それぞれのユーザの端末で、他のアプリたちとマルチタスクで使われます。Low Memory Killerによりプロセスごとkillされる可能性は、どのアプリにもあります。

そう、Activityの破棄は、開発中に出くわさなかったり、画面回転させていないアプリを作っている場合でも、
大体みんな意識しなければならない問題
なのです、、!自分は関係ないと思ってた人、、!残念!\(^o^)/

で、何が問題?

これらの場合、Activityは一旦破棄され、その後、savedInstanceStateに破棄時のインスタンス状態を
伴いながら
復元されます。(ライフサイクルはonCreateから走ります。)
その破棄時のインスタンス状態を保存するのはonSaveInstanceStateメソッドの役目です。

savedInstanceStateにデータが入ってくる = 上記のような破棄からのActivity復元を試みている
と考えてください。

さて、問題を一瞬分かりづらくさせるのが、たいていのViewは、自己再生機能を持っているという点です。

Viewの自己再生

private EditText mEditText;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_a);
    mEditText = (EditText) findViewById(R.id.et_test);
}

EditText、普通にonCreateからライフサイクルを走らせただけでは何も文字列は入力されていないハズ。
でも、破棄からの復元時は、EditTextに入力した内容は、自動で復元されます。
View自体が破棄時に状態を保存し、復元時に状態を復元する機能を持っているためです。
ViewクラスonSaveInstanceState/onRestoreInstanceState
(これらはそれぞれ、ActivityのonSaveInstanceState,onRestoreInstanceStateから呼ばれます。)

ちょっと画面回転させただけだと、上記の再生機能が働いてくれるため、「あれ、何も問題ないじゃん」って思ってしまうかもしれません。

回転させても・・・ 中身はそのまま
20170228-173357.png 20170228-173403.png

このとき、saveInstanceStateの中身をログ出力してみると、以下のような文字列が取得できます。

Bundle[{android:viewHierarchyState=Bundle[{android:views={16908290=android.view.AbsSavedState\$1@2490d10,2131427397=android.view.AbsSavedState\$1@2490d10, 2131427398=android.view.AbsSavedState\$1@2490d10, 2131427399=android.support.v7.widget.Toolbar\$SavedState@e2dec68,2131427400=android.view.AbsSavedState\$1@2490d10,2131427414=android.view.AbsSavedState\$1@2490d10,2131427415=android.view.AbsSavedState\$1@2490d10,2131427416=TextView.SavedState{8058881 start=15 end=15 text=input some text},2131427417=CompoundButton.SavedState{eb89d26 checked=true}},android:focusedViewId=2131427416}]}]

確かに、カーソルの位置的な数字とか、テキストの内容とか、チェックボックスの状態みたいなのが、受け渡されてる風です!これらを使ってViewは自己再生しているわけですね。

しかし、復元処理の中で、復元されないものもあります。

復元されないもの

例えば、Activityクラスで保持しているメンバ変数などです。
こういったものがActivityの復元時に問題になってきます。

先程のサンプルアプリに少し処理を足しました。背景を赤くする処理は一旦コメントアウトしてます。
「カウントアップ」ボタン押下でメンバ変数のintをインクリメントしていき、「確認」ボタン押下でその時点のメンバ変数の値をトースト表示するサンプルです。以下にActivityクラスのコードだけ少し抜粋します。

private Button mBtnTest;
private Button mBtnShow;
private int mTapCount = 0;

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

    ...省略

    mBtnTest = (Button) findViewById(R.id.btn_count_up);
    mBtnTest.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mTapCount++;
        }
    });

    mBtnShow = (Button) findViewById(R.id.btn_show);
    mBtnShow.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(ActivityA.this, String.valueOf(mTapCount), Toast.LENGTH_SHORT).show();
        }
    });

    ...省略

}

回転時にクリアされるメンバ変数

カウントアップをタップした回数だけ加算され トーストに値が表示されます 画面回転直後にトーストさせると・・・ メンバ変数が初期化されて値が0になってしまっています
20170228-235656.png 20170228-235731.png 20170228-235813.png 20170228-235820.png

このように、Activityでユーザが操作を行った結果得られる何らかの数値などをメンバ変数に持たせていた場合でも、Activityが破棄されてしまえば、メンバ変数はクリアされてしまいます。

復元時、ライフサイクルはonCreateから走るので、大丈夫、死にはせん、なのですが、いかんせん画面が生成された後に操作した結果を保存してあるのが当該メンバ変数の意義であって、復元時に初期値へ初期化されてしまっては元も子もありません

このように、メンバ変数をライフサイクル外のイベント(例えばボタンタップ時など)で初期化・設定していて、そのメンバ変数がある前提の処理が他の箇所にあった場合、復元時には復元しきれず、例えばヌルポが発生したり、予期しない動作が発生したりします。

そこで、これらも復元したいのであれば、ActivityのonSaveInstanceメソッドをoverrideして、保存項目を追加してやる必要があります。key-value形式でBundleに詰めます。

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("tap_count", mTapCount);
}

こうしておけば、破棄からの復元時にonCreateかonRestoreInstanceStateに渡ってくるので、取り出して、メンバ変数へ当てはめます。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_a);
    ...
    if (savedInstanceState != null) {
        int tapCount = savedInstanceState.getInt("tap_count");
        mTapCount = tapCount;
    }
    ...
}

こうすることで、メンバ変数も復元できるようになります。

破棄されても復元されるようになったメンバ変数

カウントアップをタップした回数だけ加算され トーストに値が表示されます 画面回転直後にトーストさせると・・・ メンバ変数が初期化されなくなりました!
20170301-002738.png 20170301-002803.png 20170301-002823.png 20170301-002829.png

saveInstanceState、こんな感じで使うわけです。なんとなく感じが掴めましたでしょうか。

さて、この開発時には捉えがたい構成変更・メモリ不足によるActivityの破棄を、擬似的に再現してくれる大変便利な仲間がいます。
それが、開発者オプションの「アクティビティを保持しない」「バックグラウンドプロセスの上限」なのです!

便利だぞ!開発者オプション

20170301-003401.png

「アクティビティを保持しない」

Activity起動 -> マルチタスクボタン押下 -> (Activity破棄)(例えば言語設定して戻ってきた想定) -> アプリに戻る -> Activity破棄からの復元

という「構成変更してアプリに戻ってきたらActivity破棄されたパターン」と同等な動きの検証が簡単に行えます。

起動して Activityから離れて 戻ると破棄されています
20170228-162903.png 20170228-162908.png 20170228-162914.png

「バックグラウンドプロセスの上限」

「バックグラウンドプロセスを使用しない」に設定 -> Activity起動 -> 他のアプリに切り替え -> (プロセスkill,Activity破棄)(パズドラしてたらkillされた想定) -> アプリに戻る -> プロセス起動,Activity破棄からの復元

という「メモリ不足でプロセスkillされたパターン」と同等な動きの検証が簡単に行えます。
Activityだけでなくプロセスもkillされてしまうこのパターンは、static変数なども初期化されてしまうため、static変数による動作不良の検証も行えます。

起動して 別アプリを起動して 戻ると 破棄されています
20170228-163628.png 20170228-163717.png 20170228-163730.png 20170228-163740.png

ちなみに、「バックグラウンドプロセスの上限」でググると、「もっさり感を解消するために設定してみた」
みたいな記事ばっかり出てきます。面白いことに、世間ではもっさり感を解消するために使われているようです。
理にかなっているっちゃあかなっているわけです。

まとめ

Activity破棄は誰にでも起きうる問題。開発者オプションをうまく使って検証して行きましょう。
ちなみに、Fragmentについてはまったく触れませんでしたが、根本となる考え方は同じです。(だいぶ適当か)

参考