PEEE802.11

モバイルソフトウェアエンジニアの備忘録

Androidアプリでstaticフィールドは絶対ではない

Androidアプリで、Contextをどこからでも参照する方法として以下のようなコードがよく紹介されている。

  • staticでApplication Contextを保持するクラス
public class ContextHolder {
    private static Context mContext;

    public static void setContext(Context context) {
        mContext = context;
    }

    public static Context getContext() {
        return mContext;
    }
}
  • アプリのプロセス起動時に実行されるApplicationクラスのonCreate()でContextをセット
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        ContextHolder.setContext(this);
    }
}

あとはContextHolder.getContext()で任意のクラスからContextを取得できる、というわけだ。

しかし、実はこのコードは問題を孕んでいる。

Android StudioでContextをstaticフィールドに突っ込むと「メモリリークするよ!」と警告が出るが、もっと深刻な問題は知らぬ間にnullになることである。

ご存知の通り、Androidはメモリが逼迫するとバックグラウンドにあるActivityやらServiceやらを殺していく。殺されるのはそういったAndroid特有のオブジェクトだけではなく、アプリのライフサイクルとは無縁の上記ContextHolderのようなオブジェクトも例外では無い…ように感じる。というのも、実際に上記のようにstaticで参照を保持しているフィールドにアクセスするアプリで、ぬるぽでクラッシュしたレポートがコンスタントに上がってきているので。クラスがUnloadされると、次回必要になった際に再Loadされる。その時に初期値を設定していないstaticフィールドはnullで初期化される。そこで本来はApplicationクラスのonCreate()でContextを設定したいところが、プロセスまでは殺されていないためにApplicationクラスのonCreate()が再度呼ばれず、staticフィールドはnullのままになってしまうのである。

以下にちらっと書いてあるが、アプリをロードしたClassLoaderごとGCされることがあると示唆されている。

developer.android.com

The class references, field IDs, and method IDs are guaranteed valid until the class is unloaded. Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android.

別の例として、以下のようにApplicationクラス内にstaticで持つコードも見かける。さすがにApplicationクラスはUnloadされないと思うし、されたとしても再Load時にonCreate()が呼ばれるはずなので、この場合はおそらく問題ないと思う。

public class MyApplication extends Application {
    private static Context mContext;

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
    }

    public static Context getContext() {
        return mContext;
    }
}

ClassLoaderごとUnloadされる状況をあえて作り出すのは容易ではなさそうなので、実際に動作確認したわけではなくあくまで予測ですが…