ContextのおさらいとApplicationContextをどこからでも参照できるようにする方法

  • 71
    Like
  • 1
    Comment

Androidでは、いたるところでContextが必要になる。Application#getApplicationContextで得られるContextを、どこからでも得られると便利である。便利というか必須ではないかと思う。メソッド呼び出しの深い階層でContextが必要になった時、それまでの呼び出しメソッドすべてでContextを引数で受け取るように修正したりと泥沼にハマりかねないから・・・

まずはContextについて復習

  • ActivityやServiceはContextのサブクラスである
  • ApplicationもContextのサブクラスである
  • 当然、それぞれContextの実体(メモリ上のアドレス)は別である
  • Application#getApplicationContextで得られるContextはApplicationと同じである
    • StackoverflowでApplicationのサブクラスを作った場合、異なるという話も見かけたが、実際は同じだった
    • ただし、両者が同じということに依存したコードは書く必要はないと思う

以降はApplicationContextとはApplication#getApplicationContextで得られるContextを指すものとする。

引数にContextを必要とする場合、そのContextと受け取る側のオブジェクトのライフサイクルについて考慮する必要がある。

  • Contextの寿命 >= 受け取る側のオブジェクトの寿命

となるように意識する。

  • Contextの寿命 < 受け取る側のオブジェクトの寿命

このようにするとメモリリークの危険がある。

望ましい例

  • Activityが画面表示使うViewまわりでContextを必要とする場合、ActivityそのものをContextとして渡すのが望ましい
  • シングルトン(staticオブジェクト)のように、その寿命がApplicationの寿命と同じである場合、ApplicationContextを渡すのが望ましい
  • 文字列取得などリソースは、どっちでも良いがApplicationContextを渡して問題ない場合がほとんどだろう(逆に望ましくないケースはあるのだろうか?)

メモリリークが起きる典型的ケース

  • シングルトンなどActivityを引数でContextとして受け取る
  • シングルトンはContextを必要とし続けるので、それをメンバ変数に持つ
  • シングルトンはActivityが画面上必要と無くなりonDestroyが呼ばれていてもメンバに保持し続ける
  • つまりActivityはメモリから消えない

引数として渡す側は、そのオブジェクトがContextをメンバ変数に保持し続けるのか判断がしづらいところである。ソースの中身が見られなければ全くわからない。では、一切の危険回避のために、ApplicationContextを渡せば良いのではないか。そうすればリークは起きない。と考えたくなるが、ViewまわりでApplicationContextを渡すと正常に動作しなかったり、クラッシュするケースもある。

ApplicationContextをどこからでも参照できるようにする

本題のApplicationContextをどこからでも参照できるようにする方法。ApplicationContextの寿命はApplicationの寿命と同じである。よって、Applicationオブジェクトが作られるタイミングでstaticで保持すれば良い。

public class MyContext {
   private static MyContext instance = null;
   private Context applicationContext;

   // publicをつけないのは意図的
   // MyApplicationと同じパッケージにして、このメソッドのアクセスレベルはパッケージローカルとする
   // 念のため意図しないところで呼び出されることを防ぐため
   static void onCreateApplication(Context applicationContext) {
     // Application#onCreateのタイミングでシングルトンが生成される
     instance = new MyContext(applicationContext);
   }

   private void MyContext(Context applicationContext) {
     this.applicationContext = applicationContext;
   }

   public static MyContext getInstance() {
     if (instance == null) {
        // こんなことは起きないはず
        throw new RuntimeException("MyContext should be initialized!");
     }
     return instance;
   }
}

public class MyApplication extends Application {
   // Application#onCreateは、ActivityやServiceが生成される前に呼ばれる。
   // だから、ここでシングルトンを生成すれば問題ない
   @Override
   public void onCreate() {
       super.onCreate();

       MyContext.onCreateApplication(getApplicationContext());
   }
}

これで、このようにしていつでもApplicationContextを参照できる。

MyContext.getInstance().getApplicationContext();

もっとシンプルな方法もある。検索するとよく出てくる方法である。

public class MyApplication extends Application {
    private static MyApplication instance = null;
       @Override
       public void onCreate() {
           super.onCreate();

           instance = this;
       }

       public static MyApplication getInstance() {
           return instance;
       }
    }

ApplicationContextはこのようにして取得できる。

Context context = MyApplication.getInstance().getApplicationContext();

実際は、このようにしてもいいだろう。

Context context = MyApplication.getInstance();

MyApplication < Application < Contextのように継承されているので、MyApplicationはContextとしても使えるからだ。

これはMyApplicationそのものをstaticで持ってしまう方法である。しかし自分はMyContextのようにクラスを分けることを好む。

  • Applicationオブジェクトそのものを必要とするケースは少ない
  • 必要なときに、Activity#getApplicationで対応できる場合がほとんど
  • MyApplicationに独自メソッドを追加すると、使う側がApplicationのメソッドなのか独自メソッドなのか区別がつきにくくなる
  • つまりMyApplicationが持つメソッドが多くなりすぎるとわかりにくくなる
  • MyContextはApplicationのサブクラスでないため、Applicationのメソッドはなく、使う側もすっきりしてわかりやすい

MyContextの拡張

でも、Contextを得たい時、

Context context = MyContext.getInstance().getApplicationContext();

よりは、

Context context = MyApplication.getInstance();

この方が楽じゃね?と思うかもしれません。でも、そのへんはメソッド追加すれば良いだけです。

public class MyContext {
   〜〜〜 省略 〜〜〜
   public static Context getContext() {
       return instance.getApplicationContext()
   }

これでMyContext.getContext()で呼び出せる。

文字列の取り出しもよくあるので、これもこうしてしまうと楽ちん。

public class MyContext {
   〜〜〜 省略 〜〜〜
   public static String getString(int resId) {
       return instance.getApplicationContext().getString(resId)
   }

MyContext.getString(resId)で呼び出せるようになる。

この辺とかもうシングルトンとか単一関数でいいんじゃない?みたいな話はまた別の記事で。

※ staticおじさんと呼ばないで・・・でも、ContextはSingletonであるべきだと思う。そこら中で引数で必要になるということは、結果としてContextがいろんなクラスとの結びつきを強くしてしまっているとも言えるかなと。