fc2ブログ

Android 開発備忘録 (10) アプリケーションのライフサイクル・インスタンス削除

Androidの開発で一番厄介だったのが、このアプリケーションのライフサイクルである。

縦横の切り替えをしたり、バックグラウンドから復活したときに、データが消えてしまい、
前回の状態が表示されない、あるいは部分的にしか表示されていないということがある。
あるいは、エラーがでてアプリケーションが終了してしまう。
今回の開発を通じて、他のアプリで同様の現象が起きる理由がよくわかった。

通常のJavaの開発では、インスタンスへの参照を保持しておけば、プロセスを終了しない限り、
いつでもそのインスタンスの内容を参照することはできる。

ところがAndroidでは、OSの都合で、勝手にインスタンスが削除されたり、クラスがアンロードされたり
する。
そのため再表示された際に、データがなかったり、NullPointerで落ちたりする。

Androidのシステムは、インスタンスを削除する際、あとから復旧できるように、onCreateの引数にある
Bundle(savedInstanceState)とIntentとを保存しておき、Activityを再度Createするときにそれらを
使用する。(どこに保存しているのかは不明。savedInstanceStateやIntentが巨大だとインスタンスを
解放しても空きメモリを増やす目的を遂げられないため、たぶんDiskに書いているのでは?)

保持するべき情報(一時的なもののみ)としては、
 ①Activityのインスタンス変数
 ②参照しているクラスのstatic変数
 ③画面で表示されている内容
 ④ユーザがテキストボックス等で入力した情報
等である。

考えられる事態としては、

1. 表示中のActivityのインスタンスが削除される。
2. Stackに保存されているActivityの情報が削除される(Root Activityを除く)
3. Root ActivityやApplicationインスタンスが削除される。
4. Activityの再現に必要なIntentとsavedInstanceStateが削除される。

があり、いずれもインパクトがある。

なお、4は、2が起きた場合、同時に起きる。というかリストアするActivityがなければ、IntentとsavedInstanceStateは不要となる。そもそも2なしで、4が起きるかというと、多分起きないだろう。
3も仕様上は起きないとなっているが、実際タスクキラーで終了すると、インスタンスは削除されている。
これらを明記しているドキュメントはなさそう。

また2についても、Activityのインスタンスではなく、情報と書いた理由は、Stack中に残っていても、
インスタンス丸ごと残っているわけではないのではないかという懸念から。StackにあるActivityも、
IntentとsavedInstanceStateがあれば、復旧できるのでインスタンス丸ごと残っている必要はない。
実際の動作を見てもそうなっている。

インスタンスの情報は簡単に消える。ローテートしても、一瞬だけバックグラウンドに移行しても消える。
しかし、Stack上のインスタンス情報は残っている。
Activityが生成された段階でStackに入るという説と他のActivityに移った段階で入るという説があるがどちらが正しいのだろうか。


実際に、画面遷移を重ねた状態で、バックグラウンドに移行し、タスクキラーでアプリを終了する。
そうしたとしても、ホームボタン長押しで表示されるアプリの履歴は残っているので、それをクリックする。
すると、すぐに表示していたActivityに行くのではなく、まずApplicationのonCreate()が呼ばれる。
そして次いで、当該ActivityのonCreate()が呼び出される。
Stack上のインスタンスは削除されても、Stackの情報は残っており、戻るボタンを押すと、その前のActivity
がロードされる。しかし、インスタンスは削除されているのでonCreate()が呼ばれる。

また、画面表示情報③④の情報はそれとは別に残っている。

Almost every widget in the Android framework implements this method as appropriate, such that any visible changes to the UI are automatically saved and restored when your activity is recreated.


とはいえ、長い時間が経過し、他でリソースが使われると、Stack情報も削除される。長い時間は、ドキュメント
にある30分とは限らない。1日置いても復旧はできているので、おそらくメモリなどのリソース不足等も加味され
ていると思う。実際、そういうケースでは、メインActivityからスタートする。

まとめると、以下の3つのレベルがある。

(1) 回転、もしくはバックグラウンドの移行後ある程度の時間
 表示しているActivityのインスタンスが削除される。
 それ以外は残っている。
 ある程度の時間というのは状況に依存する。場合によってはバックグラウンドに移行して即となる。

 正確には、バックグラウンドに移行すると、onSaveInstanceState(), onPause(), onStop()が呼ばれる。
 もう少し時間が経つと、状況によっては、Activityのインスタンス変数がクリアされる。Applicationのインスタンスもクリアされている。しかしonDestroy()は呼ばれない。また、そのインスタンスやメンバーのfinalize()が呼ばれる場合と呼ばれないときがある。
 
 こうなると、再開時には、ApplicationのonCreate()から呼び出される。
 しかし、回転すると確実にonDestroy()まで呼ばれる。 

(2) バックグラウンドでしばらくの時間および他でメモリ不足
 全インスタンスが削除されているがスタック情報は残っている
 この状況はシステム側で行うが、同じ状況はタスクキラーでも発生させられる。
 タスクキラーで削除した場合、onDestroy()は呼ばれない。

(3) 2がさらに進んだ場合
 スタック情報も消える。

それぞれについて対策を考える。
まず(3)の場合は対策の打ちようがない。スタック情報がないので、どのActivityを再現するかはわからない。
プログラム上でスタックを記録することはできるかもしれないが、同じ状況を作れるのかどうかは不明。
というか、その場合はもうメインActivityからでいいのでは。当然、永続的に保持する情報は都度保存しておく
必要はあるが。


対策方法:
1. android:alwaysRetainTaskState
 マニフェストファイルのメインActivityにandroid:alwaysRetainTaskState="true"を記載する。
 タスクの状態が保たれるというが全く効果なし。
 インスタンスの状態は保持されない。

2. getLastNonConfigurationInstance()
 onRetainNonConfigurationInstance()とセットで用いる。
 onRetainNonConfigurationInstance()が呼ばれる際に、保存したいオブジェクトをreturnすると、
 getLastNonConfigurationInstance()で取得できるというが、onRetainNonConfigurationInstance()は呼ばれても、
 getLastNonConfigurationInstance()で値がnull以外だった試しがない。やり方が悪いのかもしれないが全く効果なし。

3. Application継承クラスのインスタンスのメンバーに保存
 一番簡単なのは、Applicationのインスタンス変数に値を保存しておき、そこを参照するようにする方法である。
 (1)のケースについては有効だが、結局状況によって(2)の状況が発生し、その場合値はnullになってしまう。
 前回、情報の共有にApplicationを使う話をしたが、これはあくまでも一時的な情報共有で、もし(2)の事態が
 起きたら、再度データをロードする処理を入れる必要がある。

4. savedInstanceStateの使用
 これが(2)のケースでも通用する方法である。バイト配列を使用できるのでオブジェクトをシリアライズして
 保存すれば、同じ状況が再現できる。

	@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(outState);
byte[] buf = Util.toByteArray(qamanager);
if (buf != null) {
savedInstanceState.putByteArray("QAManager", buf);
}
}

public void restoreSavedInstance(Bundle savedInstanceState) {
if (savedInstanceState != null) {
byte[] buf = savedInstanceState.getByteArray("QAManager");
if (buf != null) {
qamanager = (QAManager)Util.toObject(buf);
}
}
}

 ただし、Activityやシリアイズ不可のものをインスタンスのメンバーに加えている場合、それはtransientにしてシリアライズの対象から外し、リストアの際に、再度セットするようにする。

 また、他にもstaticなメンバーなどに値をセットしている場合、再度ロードするような処理が必要である。

 最初、このやり方がよくわからず、どういうときに値が消えるのかについて、私自身混乱しており、
 かなり試行錯誤し、しばらくは、sdcardにオブジェクトをシリアライズしてファイルに保存する方法を
 取った。それをonPause()のタイミングで呼び出すようにしていた。が、結局上記の方法で問題なく
 動作するので不要となった。

 とはいえ、以下のようにonSaveInstanceState()が呼ばれる保証はないので永続データの保存には注意する必要がある。私の今回のアプリでは、セッション情報のみの保存で、永続データは処理がある都度保存していたので、特に、onPause()で処理するものは今のところない。

Note: Because onSaveInstanceState() is not guaranteed to be called, you should use it only to record the transient state of the activity (the state of the UI)?you should never use it to store persistent data. Instead, you should use onPause() to store persistent data (such as data that should be saved to a database) when the user leaves the activity.


あと、エミュレーターだとCTRL + F11, 12で回転ができるが、一番問題の画面で、なぜかデータが消えてしまう。
たぶんエミュレータのバグとしたい。実機ではそのようなことは全くない。

eitanのダウンロード先はこちら
https://play.google.com/store/apps/details?id=happie.eitan&hl=ja

関連記事
スポンサーサイト



テーマ : プログラミング
ジャンル : コンピュータ

コメント

非公開コメント

プロフィール

dayan

Author:dayan
小職は、SE(システムエンジニア)を専門としておりますが、技術的な情報を中心に、それ以外に経済関連の日記、たわいもない日記も載せていきます。
[公式HPもよろしく!]

天気予報

-天気予報コム- -FC2-
リンク
ブロとも申請フォーム

この人とブロともになる

カテゴリー
最近の記事
ブログ内検索
最近のコメント
最近のトラックバック
RSSフィード
月別アーカイブ