スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Activityのメンバ変数がクリアされてしまう現象への対策

Androidでは端末がメモリ不足になり、アプリがバックグラウンドにいる状態の場合、アクティビティのメンバ変数がクリアされてしまいます。

これはAndroidというものに長く触れている方であればご存知のことだと思います。しかし、そのクリアされるタイミングというものを端末によっては自発的に再現できなかったため、それを直に経験した人というのはさほど多くはありません。(端末によってはTask Killerを使うと再現可能)
そのため、「どのようなときにメンバ変数がクリアされ、どのようなタイミングで復帰すればいいのか」ということを意識してコーディングしていなかった人が多いのでしょう。前回の static 変数がクリアされる問題と合わせて、こういった問題に対する対処がないことがAndroidアプリケーションの品質低下に繋がっていると考えられます。

では、具体的に対処法について説明していきます。


01. Activityのメンバ変数にしてよい型には制限がある

以前も紹介したとおり、Activityは端末がメモリ不足になり、メンバ変数を保存しなければならないタイミングになると、onSaveInstanceStateメソッドがコールされ、復帰のタイミングになると onRestoreInstanceStateメソッドがコールされます。
これを利用することで、Activityのメンバ変数がクリアされる問題に対処することが可能になります。
(詳細は前回の記事へ)


しかし、ここで問題が生まれます。
onSaveInstanceStateメソッドとonRestoreInstanceStateメソッドは Bundle 型の変数に格納されるインスタンスしか保存&復帰できません。
そうなると、自然と以下の制約が生まれます。

Bundle変数に格納できない型をActivityのメンバ変数にしてはいけない

「まさかそんなバカみたいなことを・・・」と誰もが思うことでしょう。もちろん例外はあるのですが、AndroidはOSの設計上そのようになっているわけです。これこそが、「一般Javaプログラマが陥る最大の盲点」と言えるでしょう。


02. Activityのライフサイクルとメンバ変数の復帰

「Bundle変数に格納できない型はメンバ変数にしてはいけない」と上記で説明したましたが、実はそれ以外の型のメンバ変数であっても限定条件下で復帰することができます。その条件下というのは、「onCreateで初期化されてから変更されない値」です。

どういうことかというと、Activityがメモリ不足後に復帰する場合、onRestoreInstanceStateメソッドがコールされる前に、Activityの onCreate メソッドが実行されるのです。理屈としても、Activityがメモリ上から無くなっているわけですから、onCreateで再生成をしないといけないことがわかりますよね。

つまり、onCreateで初期化しているメンバ変数に関しては、メモリ不足後は onCreate で設定されている初期値に戻るわけです。「じゃあ onCreate で初期化さえすれば Bundle 型に格納できない型の変数でもActivityのメンバ変数にしてもいいじゃん!」って思うかも知れませんが、ここがちょっとした落とし穴です。

結局、「初期値」に戻るわけなので、作業途中の値は復帰されません。これが許されるのは、Activityに配置されたビューのインスタンスや、DBやプリファレンスから取得して設定した値くらいになります。しかし、これを判断できるのは実装者本人となってしまい、その実装者の力量によってはこの条件下に当てはまるメンバ変数型を正確に判断することは非常に難しいです。

そのため、私が仕事や趣味でアプリを作成しているときは「Bundle型に格納できない型をメンバ変数にするな」をルールとして推奨しています。ビューのインスタンスの取得は、findViewByIdメソッドで取得できますし、DBやプリファレンスについては基本的に Context のインスタンスさえあればどこからでも取得ができます。そのため、不都合が起きることは殆どありません。あるとすれば、BroadcastReceiverを扱う場合が考えられますが、それ以外にもしあるとすれば、それはプログラム設計や仕様に問題があるアプリケーションかもしれません。

Androidの仕様を正しく理解した上でプログラム設計や仕様決定を行うことで、高品質なアプリを作成することが可能になりますので、「Bundle型に格納できない型をメンバ変数にするな」のルールを取り入れてみましょう。


03. Activityメンバ変数の自動保存と復帰

さて、実際にこの対応を行う上で、アプリケーション規模によっては結構な問題が浮かび上がります。それは、

Activityが増えれば増えるほど、onSaveInstanceStateメソッドとonRestoreInstanceStateメソッドに保存&復帰処理を書かなければならない

5〜10画面程度ならまだしも、これが20〜50くらいになってくると話は変わってきます。しかも、Activityのメンバ変数というのは開発中に何度も変わっていきます。一度決まったらあとは変わらないものではなく、「いくらでも変わる可能性がある」という時点で修正漏れが発生する可能性が高いわけです。

実際、現場ではクラスの未使用インポートすらも残したままにする人が多いわけですから、保存&復帰処理の修正漏れなんて当たり前のように行う人がいることでしょう。更には、「Bundle型に格納できる変数」というものを、いったいどれだけの人が把握しているのか。こればかりは自動チェックする機構が現状ないので、日々レビューしていく他現状はありません。

というわけで、これを自動化してしまおうと思います。
Androidは幸いなことにCLDC準拠ではなく、Java SE準拠であるため、
リフレクションという機能を利用することができます。

では、実際にコードをみていきましょう。



/**
* アクティビティの共通機能を定義した抽象アクティビティ。
*
* @author Kou
*
*/
public class AbstractActivity extends Activity {


/**
* フィールドキー名フォーマットのクラス名とフィールド名の区切りトークン
*/
private static final String FIELD_TOKEN = ",";

/**
* フィールドキー名フォーマット
*
* 1$ = クラス名
* 2$ = フィールド名
*/
private static final String FIELD_KEY_FORMAT =
"%1$s" + FIELD_TOKEN + "%2$s";


/**
* 指定したパッケージを含むクラス名のクラスオブジェクトを取得する。
*
* @param name パッケージを含むクラス名
* @return クラスオブジェクト
* @throws IllegalArgumentException クラス名が null の場合
* @throws IllegalStateException 指定された名称のクラスがない場合
*/
private static Class<?> getClassObject(
final String name
) {

// nullの場合は例外
if (name == null) {

throw new IllegalArgumentException();

}


try {

// 指定名称のクラスオブジェクトを返す
return Class.forName(name);

} catch (final ClassNotFoundException e) {

throw new IllegalStateException(e);

}

}


/**
* 指定クラスの全スーパークラスを取得する。
*
* 指定クラスの全スーパークラスを取得し、一覧として返却する。
* 最上位のスーパークラスであるObject型を除いた全スーパークラスが返される。
*
* また、返却一覧へ指定した検索開始・終了クラスの情報を含めるかどうかを設定できる。
*
* @param startClass 検索開始クラス
* @param endClass 検索終了クラス
* @param includeStartClass 返却一覧へ検索開始クラスを含めるかどうか
* @param includeEndClass 返却一覧へ検索終了クラスを含めるかどうか
* @return 指定クラスの全スーパークラス
* @throws IllegalArgumentException 検索開始クラスが null の場合
*/
private static List<Class<?>> getSuperClasses(
final Class<?> startClass,
final Class<?> endClass,
final boolean includeStartClass,
final boolean includeEndClass
) {

// 指定クラスが null の場合は例外
if (startClass == null) {

throw new IllegalArgumentException();

}


final List<Class<?>> retClasses = new ArrayList<Class<?>>(); // 返却クラス一覧
Class<?> nowClass; // 現在のクラス


// 返却一覧へ検索開始クラス情報を含める場合
if (includeStartClass) {

// 現在のクラスに指定された検索開始クラスを設定できる
nowClass = startClass;

} else {

// 指定されたクラスのスーパークラスを設定する
nowClass = startClass.getSuperclass();

}


// 検索終了クラスが指定されていない場合
if (endClass == null) {

// スーパークラスが null
// または Object になるまで繰り返し
while ((nowClass != null)
&& !Object.class.equals(nowClass)
) {

// 一覧へ取得したスーパークラスを追加する
retClasses.add(nowClass);

// 現在のクラスのスーパークラスを取得する
nowClass = nowClass.getSuperclass();

}

} else {

// スーパークラスが null
// または 検索終了クラス
// または Object になるまで繰り返し
while ((nowClass != null)
&& !endClass.equals(nowClass)
&& !Object.class.equals(nowClass)
) {

// 一覧へ取得したスーパークラスを追加する
retClasses.add(nowClass);

// 現在のクラスのスーパークラスを取得する
nowClass = nowClass.getSuperclass();

}

// 返却一覧へ検索終了クラス情報を含める場合
if (includeEndClass) {

// 現在のクラスに指定された検索終了クラスを設定できる
retClasses.add(nowClass);

}

}


// 作成した一覧を返却する
return retClasses;

}


/**
* 指定クラスの全インスタンスフィールドを取得する。
*
* 指定クラスの非staticフィールドの一覧を取得する。
*
* @param clazz クラス
* @return 指定クラスのフィールド一覧
* @throws IllegalArgumentException クラスが null の場合
* @throws IllegalStateException フィールド取得失敗エラー時
*/
private static List<Field> getClassInstanceFields(
final Class<?> clazz
) {

// 引数が不正の場合は例外
if (clazz == null) {

throw new IllegalArgumentException();

}


try {

// 公開フィールドと非公開フィールドを取得する
final Field[] fields = clazz.getFields();
final Field[] declaredFields = clazz.getDeclaredFields();

// 返却一覧を作成する
final List<Field> retFields = new ArrayList<Field>();

// 公開フィールド分だけ処理をする
for (final Field field : fields) {

// staticの場合
if (Modifier.isStatic(field.getModifiers())) {

// 次のフィールドへ
continue;

}

// フィールドを返却一覧へ追加する
retFields.add(field);

}

// 非公開フィールド分だけ処理をする
for (final Field field : declaredFields) {

// staticの場合
if (Modifier.isStatic(field.getModifiers())) {

// 次のフィールドへ
continue;

}

// アクセス可能に設定する
field.setAccessible(true);

// フィールドを返却一覧へ追加する
retFields.add(field);

}

// 返却一覧を返す
return retFields;

} catch (final Throwable e) {

// 失敗のため例外を返す
throw new IllegalStateException(e);

}

}


/**
* 指定インスタンスの指定フィールド(公開フィールド対象)を取得する。
*
* @param instance インスタンス
* @param name 取得するフィールド名
* @return 指定インスタンスの指定フィールド
* @throws IllegalArgumentException インスタンスまたはフィールド名が null の場合
* @throws IllegalStateException フィールド取得失敗エラー時
*/
private static Field getInstancePublicField(
final Object instance,
final String name
) {

// インスタンスまたはフィールド名が null の場合は例外
if ((instance == null) || (name == null)) {

throw new IllegalArgumentException();

}

// インスタンスからクラスを取得する
final Class<?> localClass = instance.getClass();


try {

// 公開フィールドを返す
return localClass.getField(name);

} catch (final Throwable e) {

// 失敗のため例外を返す
throw new IllegalStateException(e);

}

}


/**
* 指定インスタンスの指定フィールド(publicフィールド対象)を取得する。
*
* @param clazz クラス
* @param instance インスタンス
* @param name 取得するフィールド名
* @return 指定インスタンスの指定フィールド
* @throws IllegalArgumentException インスタンスまたはフィールド名が null の場合
* @throws IllegalStateException フィールド取得失敗エラー時
*/
private static Field getInstanceDeclaredField(
final Object instance,
final String name
) {

// インスタンスまたはフィールド名が null の場合
if ((instance == null) || (name == null)) {

throw new IllegalArgumentException();

}

// インスタンスからクラスを取得する
final Class<?> localClass = instance.getClass();


try {

// 非公開フィールドを取得する
final Field retField = localClass.getDeclaredField(name);

// アクセス可能に設定する
retField.setAccessible(true);

// フィールドを返却する
return retField;

} catch (final Throwable e) {

// 失敗のため例外を返す
throw new IllegalStateException(e);

}

}


/**
* 指定されたインスタンスフィールドへ値を設定する。
*
* @param clazz 対象クラス
* @param instance 対象インスタンス
* @param name フィールド名
* @param value 設定する値
* @throws IllegalArgumentException インスタンスまたはフィールド名が null の場合
* @throws IllegalStateException メソッド取得失敗エラー時
*/
private static void setInstanceFieldValue(
final Class<?> clazz,
final Object instance,
final String name,
final Object value
) {

// 引数が不正の場合は例外
if ((instance == null) || (name == null)) {

throw new IllegalArgumentException();

}


Field field = null; // 取得したフィールド

try {

// 対象クラスがない場合
if (clazz == null) {

try {

// 指定インスタンスの公開フィールドを取得する
field = getInstancePublicField(instance, name);

} catch (final Throwable e) {

// 指定インスタンスの非公開フィールドを取得する
field = getInstanceDeclaredField(instance, name);

}

} else {

try {

// 指定クラスの公開フィールドを取得する
field = getClassPublicField(clazz, name);

} catch (final Throwable e) {

// 指定クラスの非公開フィールドを取得する
field = getClassDeclaredField(clazz, name);

}

}

// 値を設定する
field.set(instance, value);

} catch (final IllegalStateException e) {

throw e;

} catch (final Throwable e) {

throw new IllegalStateException(e);
}

}


/**
* 指定クラスの指定公開フィールドを取得する。
*
* @param clazz クラス
* @param name フィールド名
* @return 指定クラスの指定公開フィールド
* @throws IllegalArgumentException クラスまたはフィールド名が null の場合
* @throws IllegalStateException フィールド取得失敗エラー時
*/
private static Field getClassPublicField(
final Class<?> clazz,
final String name
) {

// 引数が不正の場合は例外
if ((clazz == null) || (name == null)) {

throw new IllegalArgumentException();

}


try {

// 指定フィールドを返す
return clazz.getField(name);

} catch (final Throwable e) {

// 失敗のため例外を返す
throw new IllegalStateException(e);

}

}


/**
* 指定クラスの指定非公開フィールドを取得する。
*
* @param clazz クラス
* @param name フィールド名
* @return 指定クラスの指定非公開フィールド
* @throws IllegalArgumentException クラスまたはフィールド名が null の場合
* @throws IllegalStateException フィールド取得失敗エラー時
*/
private static Field getClassDeclaredField(
final Class<?> clazz,
final String name
) {

// 引数が不正の場合は例外
if ((clazz == null) || (name == null)) {

throw new IllegalArgumentException();

}


try {

// 指定フィールドを取得する
final Field retField = clazz.getDeclaredField(name);

// アクセス可能を設定する
retField.setAccessible(true);

// 取得したフィールドを返す
return retField;

} catch (final Throwable e) {

// 失敗のため例外を返す
throw new IllegalStateException(e);

}

}


/**
* 指定されたObject型データを指定されたバンドルへ追加する。
*
* 指定された値の型を判定し、
* 適切な型であれば指定されたバンドルへ値が追加される。
* Bundleでサポートされていない型の場合は値は追加されない。
*
* @param bundle 追加先バンドル
* @param key 追加する値名称
* @param value 追加する値
* @return 値の追加に成功した場合は true
* @throws IllegalArgumentException バンドルまたは値名称が null の場合
*/
private static boolean putObjectBundle(
final Bundle bundle,
final String key,
final Object value
) {

// バンドルまたは値名称が null の場合
if ((bundle == null) || (key == null)) {

throw new IllegalArgumentException();

}


// Serializable型の場合
if (value instanceof Serializable) {

// Serializable型として追加する
bundle.putSerializable(
key,
(Serializable)value
);

// Bundle型の場合
} else if (value instanceof Bundle) {

// Bundle型として追加する
bundle.putBundle(
key,
(Bundle)value
);

// Parcelable型の場合
} else if (value instanceof Parcelable) {

// Parcelable型として追加する
bundle.putParcelable(
key,
(Parcelable)value
);

// Parcelable[]型の場合
} else if (value instanceof Parcelable[]) {

// Parcelable[]型として追加する
bundle.putParcelableArray(
key,
(Parcelable[])value
);

// その他
} else {

// 追加失敗
return false;

}


// 追加成功
return true;

}


/**
* メンバ変数の自動保存処理を行う。
*
* 本メソッドは、端末がメモリ不足などの理由でアプリをメモリ上に常駐できなくなった場合に実行される。
* 本来の Activity の仕様では各アクティビティごとにメンバ変数を保存する必要があるが、
* これは非常な手間が掛かり、かつ修正時における不具合の原因となりうる。
*
* そこで本クラスはアクティビティに定義されているメンバ変数を自動的に保存する。br>
* 自動保存できるメンバ変数は、プリミティブ型とそのラッパーと配列など
* Bundleクラスへの追加をサポートしている型変数のみとなる。
*
* @param outState 自動保存するメンバ変数の保存先 Bundle データ
*/
@Override
protected void onSaveInstanceState(
final Bundle outState
) {

try {

// 自クラスを含んだ全スーパークラス情報を取得する
final List<Class<?>> classes = getSuperClasses(
getClass(),
AbstractActivity.class,
true,
true
);

// 自クラスを含んだ全スーパークラス情報分繰り返す
for (final Class<?> nowClass : classes) {

// 全フィールドを取得する
final List<Field> fields = getClassInstanceFields(nowClass);

// 全フィールド分処理をする
for (final Field field : fields) {

// フィールドが final の場合
if (Modifier.isFinal(field.getModifiers())) {

// 次のフィールドへ
continue;

}

// パラメータを追加する
putObjectBundle(
outState,
String.format(
FIELD_KEY_FORMAT,
nowClass.getName(),
field.getName()
),
field.get(this)
);

}

}

} catch (final Throwable e) {

e.printStackTrace();

}

// スーパークラスの処理を実行する
super.onSaveInstanceState(outState);

}


/**
* メンバ変数の自動復帰処理を行う。
*
* {@link #onSaveInstanceState(Bundle)} メソッドで自動保存されたメンバ変数の
* 自動復帰処理を行う。
*
* @param savedInstanceState 自動保存されたメンバ変数を格納した Bundle データ
*/
@Override
protected void onRestoreInstanceState(
final Bundle savedInstanceState
) {

try {

final int indexClassName = 0; // クラス名インデックス
final int indexFieldName = 1; // フィールド名インデックス
final int indexCount = 2; // インデックス数

// 全キー名分処理をする
for (final String key : savedInstanceState.keySet()) {

// キー名を分割する
final String[] names = key.split(FIELD_TOKEN);

// キー名の要素がインデックス数と異なる場合
if (names.length != indexCount) {

// 次の要素へ
continue;

}

// クラス名とフィールド名を取得する
final String className = names[indexClassName];
final String fieldName = names[indexFieldName];

// 値を設定する
setInstanceFieldValue(
getClassObject(className),
this,
fieldName,
savedInstanceState.get(key)
);

}

} catch (final Throwable e) {

e.printStackTrace();

}

// スーパークラスの処理を実行する
super.onRestoreInstanceState(savedInstanceState);

}


}



ブログに載せるには少々長かったですが、このAbstractActivityクラスをアプリ内の全Activityクラスが継承して利用することで、メンバ変数の自動復帰が行えるようになります。ここに、型チェック処理などを入れて、非サポート型をメンバ変数に定義しているときに例外を出すようにすればより完璧になることかと思います。ただ、扱いには十分注意しましょう。


以上で、Activityのメンバ変数が突然クリアされる問題については終了です。static変数が突然クリアされる問題と合わせて対応することが非常に重要となります。高品質なアプリを目指すためにも、是非対応しましょう。
スポンサーサイト

コメントの投稿

非公開コメント

プロフィール

Author:Kou
モバイル関連の開発ばかりやってる人のブログです。たまにWebもやります。

最新記事
最新コメント
最新トラックバック
月別アーカイブ
カテゴリ
検索フォーム
RSSリンクの表示
リンク
ブロとも申請フォーム

この人とブロともになる

QRコード
QR
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。