2012/02/27

Android:引数はthisか?getApplicationContextか?ActivityとApplicationの違い


AndroidにはContextを引数にとるAPIが数多く存在しています。
例えばArrayAdapterのコンストラクタArrayAdapter<T>(Context, int)があります。

このコンストラクタ。Activityで使用する場合、第一引数へはthisとしてActivityContextを指定すべきでしょうか?
それともthis.getApplicationContext()としてApplicationContextを指定すべきでしょうか?

ContextはAndroidアプリを作成している場合によく使用されるクラスですが、その役割や
詳細は下記のような理由から不透明な部分が多いです。
  • Contextがアプリケーションに関する様々な情報へのインタフェースであり、非常に広
    範囲で使用されている
  • 外部、あるいはシステム側で使用されるインタフェースであり、アプリ自身でContext
    を直接操作するというシーンが極稀である
  • Contextの情報はシステムが適切に構築・処理してくれるため、アプリ開発者はContext
    の内部情報にそれほど興味を持つ必要がない

Context周辺には落とし穴があります。
例えば、下記のようなアラートダイアログを生成するコートはよく見かけると思います。
シンプルなリストを持つアラートダイアログです。
■コード1
public class MyActivity extends Activity {
// 省略
    @Override
    protected Dialog onCreateDialog(int id) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("test");
        ArrayAdapter<String> adapter
                = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        adapter.add("item");
        builder.setAdapter(adapter, null);
        builder.setPositiveButton("OK", null);
        return builder.create();
    }
// 省略
}

このActivityを定義したAndroidManifest.xmlは下記です。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="yuki.test"
        android:versionCode="1"
        android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="14" />
    <application
            android:label="@string/app_name"
            android:theme="@android:style/Theme.Holo" >
        <activity
                android:label="@string/app_name"
                android:name=".MyActivity"
                android:theme="@android:style/Theme.Holo.Light" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

このコードは下記のようなダイアログを生成します。何の変哲もないただのダイアログです。




次に、ソースコードを下記のように変更してみます。
■コード2
public class MyActivity extends Activity {
// 省略
    @Override
    protected Dialog onCreateDialog(int id) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("test");
        ArrayAdapter<String> adapter
                = new ArrayAdapter<String>(this.getApplicationContext(),  //★
                        android.R.layout.simple_list_item_1);
        adapter.add("item");
        builder.setAdapter(adapter, null);
        builder.setPositiveButton("OK", null);
        return builder.create();
    }
// 省略
}

★の箇所を変更しました。
コード1ではArrayAdapterの第一引数Contextにthis。つまりActivityインスタンスを指
定しました。
コード2ではArrayAdapterの第一引数Contextにthis.getApplicationContext。つまり
ApplicationContextを指定しました。

何か変化するのか?コードを実行して結果を確認してみます。



視認できませんが、リスト項目の文字色が黒→白に変化しました。

AndroidManifest.xmlの内容を見直してみましょう。
・ApplicationのテーマはTheme.Holo
・MyActivityのテーマはTheme.Holo.Light
となっています。
# Theme.Holoは文字色:白/背景色:黒の組み合わせ。
# Theme.Holo.Lightは文字色:黒/背景色:白の組み合わせがデフォルトです

このことから、ArrayAdapterが描画するTextViewの文字色は第一引数のContextが持つテーマ
に依存していることがわかります。
Contextはアプリリソース(ここではテーマ)にアクセスするために利用されているという
ことです。
また、ContextインスタンスであってもActivityContextと、ApplicationContextでは
動作の異なるケースがあることがわかりました。

「一部の色がおかしい」「一部にテーマが適用されない」といった現象はContext
が原因である可能性も考慮した方がよさそうですね。


次に、AlertDialog.Builderコンストラクタの引数にApplicationContextを渡してみましょう。
■コード3
public class MyActivity extends Activity {
// 省略
    @Override
    protected Dialog onCreateDialog(int id) {
        AlertDialog.Builder builder
                = new AlertDialog.Builder(this.getApplicationContext());  //★
        builder.setTitle("test");
        ArrayAdapter<String> adapter
                = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        adapter.add("item");
        builder.setAdapter(adapter, null);
        builder.setPositiveButton("OK", null);
        return builder.create();
    }
// 省略
}

★の箇所を変更しました。
コード1ではBuilderの第一引数ContextにActivityを指定していました。
コード3ではBuilderの第一引数Contextにthis.ApplicationContextを指定しました。
両方ともContextクラスを継承したインスタンスです。

コードを実行して結果を確認してみます。

ログ:
01-06 05:52:22.276: E/AndroidRuntime(4578): Caused by:
   android.view.WindowManager$BadTokenException:
     Unable to add window -- token null is not for an application

ApplicationContextでは適切なWindowTokenが得られないためエラーが発生しました。
このことから、ContextインスタンスであってもActivityContextと、ApplicationContext
には違いがあり、APIによってはエラーとなることがわかりました。


Contextについて少し触れたところで、Contextの詳細に一歩踏み込んでみます。

●Context概要
Contextはアプリケーションのグローバル情報へアクセスするためのインタフェースです。
アプリケーションのグローバル情報とは...
・パーミッション
・アプリリソース情報
・アプリ情報(コンポーネント名、プロセス名、タスクアフィニティ、テーマ等)
・ファイルリソース(DBを含む)
等々です。

Contextを取得することでこれらのグローバル情報へアクセスすることが可能になります。
他アプリのContextを経由して画像リソースを参照したり、パーミッションの有無をチェ
ックすることが可能となります。
ウィジェットコンポーネントがコンストラクタの引数でContextを求めるのはリソースへ
の参照を得るためです。
また、アクティビティの起動、ブロードキャスト、インテントの受信等の操作が可能とな
ります。

Context関連の型階層は簡略化すると下記のようになっています。

Context
└ ContextWrapper
    ├ Application
    ├ ContextThemeWrapper
    | └ Activity
    └ Service

●Contextのライフサイクル
ApplicationContextはアプリケーションのライフサイクルと連動し、ActivityContextは
アクティビティのライフサイクルと連動しています。
つまり、2つはどちらもContextインスタンスではありますが、中身は全く異なるものに
なります。

ContextのライフサイクルについてはDeveloperページのgetApplicationContextが参考に
なります。
以下要約です。
BroadcastReceiverを登録するregistReceiverを使用するケースにおいて...
・ActivityContextを使った場合
レシーバはActivityと関連付けて登録されます。システムはActivityが破棄される
前に登録したBroadcastReceiverがunregistReceiverされることを期待します。
もしunregistReceiverされなかった場合は、フレームワークがこれをクリーンナップし、
エラーログを出力します。

・ApplicationContextを使った場合
レシーバはApplicationと関連付けて登録されグローバルな状態となる。
そのためフレームワークがこれをクリーンアップすることがなく、unregistReceiverを
忘れると容易にリークしてしまいます。
参考:http://developer.android.com/reference/android/content/Context.html#getApplicationContext()

Contextを渡す対象のAPIは、ActivityかApplicationどちらが良いのかについては、
ライフサイクルを意識することも重要であることが上記からわかります。


●Context保持によるメモリリーク問題
Contextに関するメジャーな問題としてメモリリークがあります。
前述からもわかるようにContextへの参照はActivityへの参照でもある可能性があります。

下記のようにActivityContextを長期間保持するようなコードは簡単にメモリリークを引
き起こす可能性があります。
private static Context mContext;
// ...
mContext = activityInstance;
ActivityContextはActivityのライフサイクルと連動していることを思い出してください。
private staticなmContextは、これを保持するActivityのライフサイクルが終了したとして
も、mContextによるActivityへの参照は破棄されず、メモリリークが発生します。

Contextへの参照を使用する場合に留意するべき点は下記となります。
  • Contextの参照は関連するContextのライフサイクルに合わせる
    (ApplicationContextならApplicationライフサイクルに、ActivityContextであれば
    Activityのライフサイクルに合わせること)
  • 可能であればApplicationContextの使用を考える
  • Activity内部に非staticなインナークラスの作成は極力避ける。
  • 可能であればActivityへの参照は弱参照を用いる

Context参照がリークした場合、Activityとそれに紐付くインスタンス(特にViewやBitmap等)
が諸々リークするため、予想以上に大量のメモリリークとなる恐れがあります。
場合により、ガベージコレクタによるインスタンス回収が間に合わずOutOfMemory例外が
発生することもあります。(ガベージコレクタはメモリリーク保証しない)

Contextにまつわるメモリリーク問題のより詳細な情報は下記が参考になります。
http://developer.android.com/resources/articles/avoiding-memory-leaks.html


●getBaseContextについて
getBaseContext()については、ドキュメントが見当たらず詳細については掴めません
でした。
他サイトで「グーグルエンジニアが使用しないほうが良いと言っていた」という記事は見
つけましたが、根拠までは記載されていませんでした。

少し調べた結果、android.app.Dialogクラスで下記のようなコードを発見しました。
/**
* @return The activity associated with this dialog, or null if there is no associated activity.
*/
private ComponentName getAssociatedActivity() {
    Activity activity = mOwnerActivity;
    Context context = getContext();
    while (activity == null && context != null) {
        if (context instanceof Activity) {
            activity = (Activity) context;  // found it!
        } else {
            context = (context instanceof ContextWrapper) ?
                ((ContextWrapper) context).getBaseContext() : // unwrap one level
                null;                                         // done
        }
    }
    return activity == null ? null : activity.getComponentName();
}

これから推測するに、Contextは多重にラッピングされており、getBaseContext()はそれを
1つずつ剥がす(アンラップする)操作になるようです。
# Activityのクラス階層をみると2重ラップされているのかな...?

getBaseContextについて、まだまだ不明点が多いですが必要に迫られない限りは
ActivityContextかApplicationContextを使用したほうが無難そうです。

以上です。