アプリがバックグラウンド時にstatic変数が破棄されてしまうのであれば、AndroidではSingletonパターンが実現できないことになってしまうのでしょうか。いいえ、そのようなことはありません。
Applicationはプロセスと関連付けられており、プロセスが終了するまでApplicationの参照ツリーは開放されません。つまり、Singletonにしたいインスタンスは、Applicationの参照ツリーにstatic変数ではなく、インスタンス変数として保持させます。
Singletonのインスタンスは、参照ツリーのどこに保持しても構いません。上図では深い位置に保持していますが、実際にはApplicationの直下か、数が多いようであれば、その1つ下の階層辺りでしょうか。
以下はApplicationの参照ツリーにインスタンスを持たせたSingletonの実装です(今回のサンプルアプリから抜粋)。
public class AndroidSingleton { private final long mCreateTimestamp; private AndroidSingleton() { mCreateTimestamp = System.currentTimeMillis(); } public static AndroidSingleton getInstance() { return MainApplication.getInstance().getAndroidSingleton(); } @Override public String toString() { String time = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS", Locale.getDefault()).format(new Date(mCreateTimestamp)); return getClass().getSimpleName() + ": " + time; } }
getInstance()でApplicationから参照を返しているのが分かると思います。ポイントはApplication自体もSingletonになっていて、いつでもgetInstance()で参照を取得できるようにしているところです。Contextの引き回しが面倒な場合、このパターンを使えばContextを引き回す手間がかなり軽減されるはずです。
Activityが復元される際のパターンは、以下の3通りあります。
前述の「Singletonパターンのベストプラクティス」を使えば、上記2.のパターンは考慮する必要はなく、「プロセスが残っている状態で復元されるのか」「復元時にプロセスが新たに作られるのか」という違いを判別して振る舞いを変える必要があります。前者であれば復元前のオブジェクトが存在する、後者であれば存在しないとも言い換えられます。
「Applicationはプロセスと関連付けられている」と説明しましたが、今回はその性質を利用します。ActivityはプロセスIDを保持しておき、復元時にプロセスIDに変更があるかどうかをチェックすることでプロセスが再起動されたことを検知可能です。今回のサンプルアプリのコードを抜粋して説明します。
public class MainActivity extends AppCompatActivity { private int mMyPid; @Override protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { mMyPid = savedInstanceState.getInt("myPid"); } int pid = android.os.Process.myPid(); if (mMyPid == 0) { // 初回起動時の処理 } else if (mMyPid != pid) { // プロセス再起動時の処理 } else { // Activity復元時の処理 } mMyPid = pid; super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("myPid", mMyPid); } }
「mMyPid」というプロセスID保持用のフィールドに自身のプロセスIDを保持します。このフィールドはonSaveInstanceState()で保存し、Activityが復元された際にはこの値自体も復元します。復元した値と現在のプロセスIDが異なっていた場合はプロセスが再起動したことを意味します。
プロセスIDの検出処理を、super.onCreate()やsetContentView()の呼び出しに先立って行っているのは、場合によってはActivityの状態を復元せず、初回起動時と同じ振る舞いにした方が都合が良いことがあるためです。
初回起動時と同じ振る舞いにする方法はアプリの実装ごとに方法が異なるでしょう。1つは「復元した状態を破棄する」こと、もう1つは「このプロセスは自身で破棄し、PendingIntentなどで新規にプロセスとタスクを作る」ことが思い当たります。前者で可能であれば、前者が望ましいですが、FragmentManagerがフラグメントを復元しようとしてうまくいかないケースもあると思います。
今回はSingletonを実装するベストプラクティスと、Activityの復元時のベストプラクティスを解説しました。ただし、今回紹介したベストプラクティスは「Singletonパターンを利用して、複数のActivityやFragmentで状態共有/データ共有を行いたい」という要件が前提にあってのもので、そうでない場合は「それぞれのActivity/Fragmentの状態はonSaveInstanceState()で保持し、onCreate()で復元する」というのがやはり正攻法です。
また、今回紹介したベストプラクティスは、実はAndroidのバッドノウハウであるとも言えます。このようなことを知っておかなければ問題に遭遇する可能性があるAndroidは、難しいプラットフォームである一方、「他者に差を付けるチャンスがあるプラットフォーム」ともいえるでしょう。
最後になりますが、今回解説した「プロセスは残っていてstatic変数は破棄される」という現象に関する正式なドキュメントは見当たりませんでした。ドキュメントの所在や仕様に関してご存じの方、筆者の見解が異なっている場合は、編集部までご一報いただけると幸いです。
株式会社ゆめみ所属のエンジニア。Applet、デスクトップJava、サーバサイドJavaの業務開発を経て、ケータイJava、組み込みJavaから現在はAndroidを中心にJavaに関わる。他の執筆記事は「Androidで動く携帯Javaアプリ開発入門」「携帯アプリを作って学ぶJava文法の基礎」など。iOS開発もたしなみ、Java以外ではHaskellやC/C++、Luaを好む。
Copyright © ITmedia, Inc. All Rights Reserved.