連載
» 2016年11月25日 05時00分 UPDATE

実業務でちゃんと使えるAndroidアプリ開発入門(3):Javaでの常識が通用しないAndroidにおけるメモリ管理の注意点 (1/2)

本連載では、バージョンの違いに左右されないスタンダードなアーキテクチャで実業務で使えるAndroidアプリ開発のノウハウを提供していきます。今回は、Androidアプリのメモリ管理に関して、Javaとはどのような挙動の違いがあるのかをサンプルアプリを通じて解説していきます。

[緒方聡,株式会社ゆめみ]

Androidの設計思想とはどのようなものか

 本連載「実業務でちゃんと使えるAndroidアプリ開発入門」では、バージョンの違いに左右されないスタンダードなアーキテクチャで、セキュリティやパーミッション、テストのしやすさ、開発効率の向上などを考慮した、実業務で使えるAndroidアプリ開発のノウハウを提供していきます。前回の「知らずに作って大丈夫?Androidの基本的なライフサイクルイベント31選」では、Androidアプリ開発において必ず押さえておかなければならないライフサイクルイベントについて解説しました。今回は、Androidアプリのメモリ管理に関する注意事項に関して解説します。

 Javaでは「常識」とされる挙動が、Androidではそうはいかない場合があります。Javaとは異なる設計思想により、Javaのような動作を期待すると思わぬバグとなる可能性があるのです。Androidの設計思想とはどのようなものか。それによってJavaとはどのような挙動の違いが見られるのかに関して、サンプルアプリを通じて解説していきます。

サンプルアプリのダウンロード

 今回のサンプルアプリは以下よりダウンロードしてください。

サンプルアプリのソースコード

 ソースコードは以下のレポジトリからクローンしてください。

 今回は「No03a」「No03b」にプロジェクトがコミットされています。それぞれが以下の役割を持っています。

サンプルアプリの概要

No03a(左)とNo03b(右)

 No03aはバックグラウンドプロセスを強制終了させるアプリです。このアプリでNo03bを強制終了させます。No03bは強制終了される側で、どのように実装すれば整合が保てるかを検証するアプリです。No03aは、上図を見れば分かる通り、No03b以外のアプリも終了させるので、気になるアプリの挙動も同時に確認できます。

※ただし、今回の検証アプリでは、後述するプロセスは残っていてstatic変数は破棄されるパターンは確認できません。

起動/終了モデルにおけるJavaアプリとAndroidアプリの違い

 PC上のJavaアプリは、以下のように起動/終了します。

  1. Java VMがプロセスとして起動される
  2. Java VMに渡された引数が解釈され、アプリが起動する
  3. アプリが終了する際にJava VMをアプリが終了させる

 ここで重要なのは、「Javaアプリ1つに対しJava VMが1つ起動されること」「アプリ終了時にはJava VMも終了すること」です。

 一方で、Android上のアプリは以下のように起動/終了します。

  1. 「Zygote」というVMプロセスが起動する
  2. アプリはActivityManager経由でZygoteプロセスがフォークされ起動する
  3. アプリ終了時はVMやプロセスを終了させない

 「Zygote」というのは聞き慣れない単語かもしれません。生物学で「接合子」という意味を持ち、Androidでは全てのアプリプロセスの親プロセスに当たります。前述のActivityManagerもZygoteからフォークされています。

 Zygoteという親プロセスから子プロセスがフォークされるAndroidのモデルの何が良いかと言うと、1つは「全てのアプリで共通のクラスライブラリが初期化された状態からフォークされる」つまり「アプリの起動が速い」ということです。もう1つは「全ての子プロセスで親プロセスのメモリ空間を共有できる」つまり「省メモリである」という点です。限られたリソース内でたくさんのアプリを起動する必要があるスマートフォン向けOSとして、良く考えられた設計になっています。

 なお、Androidではない組み込みJavaでは「OSGi」というフレームワークを使用して、Androidのような高速起動、省メモリを実現しています。OSGiは組み込み以外の用途でも利用されていて、有名なのはEclipseでしょうか。OSGiに興味のある方は 記事「EclipseやSpringで使われている基盤技術OSGiとは」を参照してみてください。

Androidのメモリ管理モデル

 前章で記載したように、Androidは限られたリソース内でたくさんのアプリを同時に起動できるようになっています。ただ、なってはいますが、それでも限度はあります。本章では「Androidでは、アプリ実行時にメモリが足りなくなった場合、どのようなことが起こるのか」を解説していきます。

 アプリがバックグラウンド時に他のアプリがメモリを要求した場合、いくつかの段階を踏んで空きメモリを確保します。

  1. メモリ開放要求:Activity#onLowMemory()、Activity#onTrimMemory(int)が呼び出され、バックグラウンドプロセスに対してメモリ開放を要求する
  2. Activity破棄要求:Activity#onDestroy()が呼び出され、Activityがメモリ上からなくなる。ただし、Activity#onSaveInstanceState(Bundle)が事前に呼び出され、必要な状態は保存される
  3. プロセス破棄要求:android.os.Process#killProcess(int)でプロセスが終了される。この場合、実行中のActivityは終了するがonDestrory()は呼び出されない。アプリがonStop()で停止している状態であれば、onSaveInstanceState(Bundle)は呼び出された後となるため、Activityの状態復元は行える

 上述のメモリ開放要求のonLowMemory()やonTrimMemory(int)は、デフォルトでFragmentを開放する動作になります。アプリに開放可能なメモリがあれば、これらメソッドがコールバックされたタイミングで開放してもいいのですが、確保されている不要なメモリなんてないだろうから、何を開放すべきか難しいところです。アプリとしては、本来であればFragmentですら作り直す必要があるため開放されたくはないのですから。

 Activityは、noHistory属性を付与していない場合、「Recent App」に一覧として現れます。この一覧にあるActivityは、onStop()の状態のものもあれば、onDestory()まで実行されているものもあり、見た目ではどちらか判断できません。一覧から任意のActivityを選択すると、onStop()状態のものであれば、onRestart()を経由してonStart()、onResume()と状態が遷移します。onDestroy()まで実行済みのものは、新たなActivityでonCreate()から呼び出され、引数のsavedInstanceStateに保存した状態が渡されて来るので、それを基に復元します。

 今回の解説の最も重要なポイントに進む前に、Androidアプリにおけるメモリのイメージを可視化しておきます。

 上図はAndroidアプリのヒープ内で、「◯」はオブジェクトだと考えてください。オブジェクトはオブジェクトの参照を持ち、その参照を上図では線で表しています。オブジェクトの参照ツリーには、「Activity」を起点とするツリー、「Application」を起点とするツリー、「Others」に分けられます。Othersは、ここではstatic変数だと考えてください。

 上図のActivityはタスク、Applicationはプロセスに関連付けられていると考えても問題ありません。

 Activity#onDestroy()が呼び出されると、Activityを起点とする参照ツリーは破棄されます。これは問題ないと思います。その際、Applicationの参照ツリーとOthersはヒープに残ったままなので、Activityの復元時も問題なく前の状態に戻すことができます。

 アプリがバックグラウンド時にアプリのプロセス破棄が行われると、Applicationの参照ツリーだけではなく、ヒープ内の全てのオブジェクトが破棄されます。この状態でRecent App一覧からActivityを復元すると、Applicationが先に生成され、その後Activity#onCreate()が呼び出されます。復元されるActivityからすると、Applicationの参照ツリーも、Othersのstatic変数も、復元前と状態が異なるため、その点に注意して状態復元する必要があります。

 そして最も注意しなければならないのは、「メモリ使用状況によってはApplicationの参照ツリーだけ残り、Activityの参照ツリーとOthersのstatic変数は開放された状態にもなり得る」という点です。static変数はSingletonパターンを実装する際に使用しますが、以下のように実装することが多いと思います(今回のサンプルアプリから抜粋)。

public class NormalSingleton {
    private static final NormalSingleton sInstance = new NormalSingleton();
    private final long mCreateTimestamp;
 
    private NormalSingleton() {
        mCreateTimestamp = System.currentTimeMillis();
    }
 
    public static NormalSingleton getInstance() {
        return sInstance;
    }
 
    @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;
    }
}

 このように作ったNormalSingletonは、Javaであれば、アプリを終了するまでsInstanceは同じインスタンスであり、toString()が返す値は常に同じことが保証されますが、Androidでは保証されません。アプリプロセスが起動中、static変数は開放される可能性があるため、特にSingletonパターンはJavaと同じように作ってはいけません。次章で解説します。

 これまでをまとめます。

  1. Androidアプリのメモリは、大きく3種、Activityが起点の参照ツリー、Applicationが起点の参照ツリー、そのどちらでもないもの(ここではstatic変数とします)に分類できる
  2. Androidのシステムによって開放されるメモリは、以下の組み合わせがある
    • Activityの参照ツリーだけ
    • Activityの参照ツリーとその他
    • Activityの参照ツリーとApplicationの参照ツリーとその他(つまり全て)
  3. static変数も開放されることがある点に注意が必要
       1|2 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。