#はじめに。
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]
(https://developer.android.com/training/basics/activity-lifecycle/recreating.html?hl=ja)
Activityが再作成される構成変更の一覧は[こちら(Android Developers)]
(https://developer.android.com/guide/topics/manifest/activity-element.html?hl=ja#config)に定義されています。
###具体例
- 設定 -> 言語と入力 -> 言語 から言語を切り替えた時
- 設定 -> ディスプレイ -> フォントサイズ からフォントサイズを切り替えた時
- 画面回転時
Activityが破棄→復元されると背景が赤黒くなる痛ましいサンプルアプリを作りました。ソースはこちら。
画面回転
起動して | 回転すると死にます |
---|---|
言語設定
起動して | 言語設定に行って | 言語を切り替えて | 戻ってくると | 破棄されています |
---|---|---|---|---|
##2.メモリ不足とかでプロセスごとkillされた時
AndroidにはLow Memory Killerという仕組みが備わっており、フォアグラウンドのアプリでメモリが足りなくなった場合に、バックグラウンドに居る要らないアプリ(プロセス)をバッタバッタと切り捨てていきます。
こうして、あなたのアプリも切られ対象になると、プロセスごとkillされ、Activityも破棄されてしまいます。
その後、ホームのアイコンなどから当該アプリ起動しようとすると、別プロセスで新しく起動するとともに、破棄されたActivityは復元されます。
起動して | いろんなアプリ起動してしばらくして戻ってくると | 破棄されていることがあります |
---|---|---|
いかがでしょうか。
要は、システム的な都合で一旦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から呼ばれます。)
ちょっと画面回転させただけだと、上記の再生機能が働いてくれるため、「あれ、何も問題ないじゃん」って思ってしまうかもしれません。
回転させても・・・ | 中身はそのまま |
---|---|
このとき、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になってしまっています |
---|---|---|---|
このように、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;
}
...略
}
こうすることで、メンバ変数も復元できるようになります。
破棄されても復元されるようになったメンバ変数
カウントアップをタップした回数だけ加算され | トーストに値が表示されます | 画面回転直後にトーストさせると・・・ | メンバ変数が初期化されなくなりました! |
---|---|---|---|
saveInstanceState、こんな感じで使うわけです。なんとなく感じが掴めましたでしょうか。
さて、この開発時には捉えがたい構成変更・メモリ不足によるActivityの破棄を、擬似的に再現してくれる大変便利な仲間がいます。
それが、開発者オプションの「アクティビティを保持しない」「バックグラウンドプロセスの上限」なのです!
##「アクティビティを保持しない」
Activity起動 -> マルチタスクボタン押下 -> (Activity破棄)(例えば言語設定して戻ってきた想定) -> アプリに戻る -> Activity破棄からの復元
という**「構成変更してアプリに戻ってきたらActivity破棄されたパターン」**と同等な動きの検証が簡単に行えます。
起動して | Activityから離れて | 戻ると破棄されています |
---|---|---|
##「バックグラウンドプロセスの上限」
「バックグラウンドプロセスを使用しない」に設定 -> Activity起動 -> 他のアプリに切り替え -> (プロセスkill,Activity破棄)(パズドラしてたらkillされた想定) -> アプリに戻る -> プロセス起動,Activity破棄からの復元
という**「メモリ不足でプロセスkillされたパターン」**と同等な動きの検証が簡単に行えます。
Activityだけでなくプロセスもkillされてしまうこのパターンは、static変数なども初期化されてしまうため、static変数による動作不良の検証も行えます。
起動して | 別アプリを起動して | 戻ると | 破棄されています |
---|---|---|---|
ちなみに、「バックグラウンドプロセスの上限」でググると、「もっさり感を解消するために設定してみた」
みたいな記事ばっかり出てきます。面白いことに、世間ではもっさり感を解消するために使われているようです。
理にかなっているっちゃあかなっているわけです。
#まとめ
Activity破棄は誰にでも起きうる問題。開発者オプションをうまく使って検証して行きましょう。
ちなみに、Fragmentについてはまったく触れませんでしたが、根本となる考え方は同じです。(だいぶ適当か)
##参考
-
アクティビティの再作成 | Android Developers|
公式。一番正確だと思われます。 -
Android の罠 [1] ちゃんと onSaveInstanceState する | Qiita
saveInstanceStateについて簡潔にまとめられています。icepickというライブラリを使って復元を便利にしています。 -
onSaveInstanceStateでインスタンスを保存する | TechBooster
安定のTechBoosterさん。 -
Android Bundle で状態を保存 | Y.A.M の 雑記帳
安定のyanzmさん。 -
Androidのプロセスがkillされる基準 | Happy My Life
Low Memory Killerという仕組みを学びました。 -
LowMemoryKillerによりkillされる閾値について | Yukiの枝折
killの閾値について。前面にある方が落ちにくい、ホームアプリは若干落ちにくいなど、アクティビティの状態に応じて優先順位が切り替わっていくようです。