アプリのメモリを管理する

ランダムアクセス メモリ(RAM)はどのソフトウェア開発環境でも貴重なリソースですが、物理メモリが制限されることが多いモバイル オペレーティング システムではさらに貴重になります。Android ランタイム(ART)と Dalvik 仮想マシンはどちらも、ガベージ コレクションを定期的に実行しますが、だからと言って、アプリがメモリの割り当てと解放を行うタイミングや場所を無視してよいわけではありません。引き続き、通常は静的なメンバー変数でのオブジェクト参照を維持することによって引き起こされるメモリリークが発生しないようにし、ライフサイクル コールバックで定義されているように適切なタイミングで Reference オブジェクトを解放する必要があります。

このページでは、アプリ内のメモリ使用量を積極的に削減する方法について説明します。Android オペレーティング システムでのメモリ管理方法について詳しくは、Android のメモリ管理の概要をご覧ください。

使用可能なメモリとメモリ使用量を監視する

アプリのメモリ使用量に関する問題を解決するには、最初に問題を特定する必要があります。Android Studio のメモリ プロファイラを使用すると、以下の方法でメモリの問題を特定して診断できます。

  1. アプリによるメモリの割り当て方法を長期的に監視します。メモリ プロファイラでは、アプリのメモリ使用量、割り当てられている Java オブジェクトの数、ガベージ コレクションの実行タイミングがリアルタイムでグラフに表示されます。
  2. ガベージ コレクション イベントを開始して、アプリの実行中に Java ヒープのスナップショットを取得します。
  3. アプリのメモリ割り当てを記録し、割り当てられたオブジェクトをすべて検査して各割り当てのスタック トレースを表示し、Android Studio エディタで対応するコードに移動します。

イベントに反応してメモリを解放する

Android のメモリ管理の概要で説明したように、Android ではさまざまな方法でアプリからメモリを再利用できます。あるいは、必要に応じてアプリを完全に強制終了し、重要なタスクを実行するためにメモリを解放することもできます。ComponentCallbacks2 インターフェースを Activity クラスに実装すると、システムメモリのバランスを細かく調整して、システムがアプリプロセスを強制終了せずに済むようにできます。また、用意されている onTrimMemory() コールバック メソッドを使用すると、アプリがフォアグラウンドとバックグラウンドのどちらで実行されているときでも、メモリ関連のイベントをリッスンして、アプリのライフサイクルや、システムでメモリを再利用する必要があることを示すシステム イベントに反応してオブジェクトを解放できます。

たとえば以下に示すように、メモリ関連の各種イベントに反応するように onTrimMemory() コールバックを実装できます。

Kotlin

import android.content.ComponentCallbacks2
// Other import statements ...

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    override fun onTrimMemory(level: Int) {

        // Determine which lifecycle or system event was raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements ...

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event was raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

onTrimMemory() コールバックは Android 4.0(API レベル 14)で追加されました。これより前のバージョンでは、onLowMemory()TRIM_MEMORY_COMPLETE イベントとほぼ同じ)を使用できます。

必要なメモリ使用量を確認する

複数のプロセスを実行できるようにするには、アプリごとに割り当てるヒープサイズのハードリミットを Android で設定します。正確なヒープサイズの上限は、デバイス全体で使用可能な RAM の量に基づき、デバイスごとに異なります。アプリがヒープ容量に達して、さらにメモリを割り当てようとすると、システムによって OutOfMemoryError がスローされます。

メモリ不足を回避するために、システムに対してクエリを実行し、現在のデバイスで使用可能なヒープスペースの量を確認できます。この量をシステムに問い合わせるには getMemoryInfo() を呼び出します。ActivityManager.MemoryInfo オブジェクトが返されます。このオブジェクトから、デバイスの現在のメモリ状態に関する情報(メモリの空き容量、メモリの合計容量、メモリの閾値(システムがプロセスの強制終了を開始するメモリレベル)など)を確認できます。ActivityManager.MemoryInfo オブジェクトには単純なブール値である lowMemory も含まれており、この値からデバイスのメモリが不足しているかどうかを判断できます。

次のコード スニペットは、アプリケーションで getMemoryInfo() メソッドを使用する方法の例を示しています。

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work ...
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work ...
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

メモリ効率の高いコード構造を使用する

一部の Android 機能、Java クラス、コード構造は、他より多くのメモリを使用する傾向があります。コード内でメモリ効率の高い別の方法を選択することで、アプリのメモリ使用量を最小限に抑えることができます。

サービスの利用頻度を抑える

必要のないときにサービスを実行したままにしておくことは、Android アプリで起こり得る、メモリ管理に関する最悪のミスの一つです。アプリがバックグラウンドでの作業にサービスを必要としていても、ジョブを実行する必要がない限り、サービスを実行したままにしないでください。タスクが完了したら、必ずサービスを停止してください。そうしないと、誤ってメモリリークを引き起こす可能性があります。

サービスを開始すると、システムは常にそのサービスのプロセスを実行し続けようとします。この性質により、サービスによって使用される RAM が他のプロセスで使用できないままになるため、サービスのプロセスの負荷が非常に大きくなります。そのため、システムが LRU キャッシュ内で維持できるキャッシュ プロセスの数が減少し、アプリの切り替え効率が低下します。メモリの空き容量が少なく、実行中のすべてのサービスをホストするのに十分なプロセスを維持できない場合、システムでスラッシングが発生することもあります。

一般に、メモリを常に利用できる状態にすることが求められるため、サービスをいつまでも利用し続けないようにする必要があります。代わりに、JobScheduler などを実装することをおすすめします。JobScheduler を使用してバックグラウンド プロセスのスケジュールを設定する方法について詳しくは、バックグラウンド処理の最適化をご覧ください。

サービスを利用する必要がある場合、その有効期間を制限するには、IntentService を使用することをおすすめします。このクラスは、サービスを開始したインテントの処理が完了するとすぐに終了します。詳しくは、バックグラウンド サービスでの実行をご覧ください。

最適化されたデータコンテナを使用する

プログラミング言語が提供するクラスの一部は、モバイル デバイスでの使用に最適化されていません。たとえば、HashMap の一般的な実装では、マッピングごとに別々のエントリ オブジェクトが必要になるため、メモリ効率が大幅に低下する可能性があります。

Android フレームワークには、最適化されたデータコンテナがいくつか含まれています(SparseArraySparseBooleanArrayLongSparseArray など)。たとえば、SparseArray クラスを使用すると、システムでキーと値の自動ボックス化(これにより、エントリごとに 1 つまたは 2 つのオブジェクトがさらに作成される)が不要になるため、効率性が向上します。

必要に応じて、いつでも生の配列に切り替えて、極めてシンプルなデータ構造にすることができます。

コードの抽象化に注意する

抽象化はコードの柔軟性と保守性の向上に役立つため、プログラミングに関するおすすめの方法としてよく使用されます。ただし、抽象化は大きな犠牲を伴うため、一般には、実行する必要があるコードの量がかなり多くなり、コードをメモリにマッピングするのに多くの時間と RAM が必要になります。そのため、抽象化によるメリットがあまりない場合は、抽象化の使用は避けるべきです。

シリアル化されたデータに対して lite 版のプロトコル バッファを使用する

プロトコル バッファは、Google が設計した、構造化データをシリアル化するためのメカニズムです。言語やプラットフォームに依存せず、拡張することも可能です。XML に似ていますが、規模、処理速度、複雑さの点で XML より優れています。データに対してプロトコル バッファを使用する場合は、クライアント側のコードでは常に lite 版のプロトコル バッファを使用してください。通常のプロトコル バッファでは極めて冗長なコードが生成されるため、アプリに各種の問題(RAM の使用量の増加、APK サイズの大幅な増大、実行速度の低下など)が発生する可能性があります。

詳細については、プロトコル バッファの README の「Lite 版」セクションをご覧ください。

メモリチャーンを回避する

前述のように、ガベージ コレクション イベントは、アプリのパフォーマンスに影響しません。ただし、ガベージ コレクタとアプリケーション スレッド間のやり取りが必要なため、短時間の間に多数のガベージ コレクション イベントが発生して、バッテリーがすぐに消費され、フレームのセットアップ時間がわずかに増加することがあります。システムがガベージ コレクションに費やす時間が多くなるほど、バッテリーの消耗が速くなります。

メモリチャーンが発生すると、多くの場合、ガベージ コレクション イベントが発生する回数が増加します。メモリチャーンは実際のところ、一定時間内に割り当てられた一時オブジェクトの数を表します。

たとえば、for ループ内で複数の一時オブジェクトが割り当てられることがあります。あるいは、ビューの onDraw() 関数内で新しい Paint オブジェクトや Bitmap オブジェクトが作成されることもあります。どちらの場合も、アプリによって大容量のオブジェクトが短時間で多数作成されます。 これらのオブジェクトによって若い世代の使用可能なメモリがあっという間にすべて消費され、ガベージ コレクションが強制的に実行されます。

言うまでもなく、コードの修正を行うには、メモリチャーンがよく発生する場所を特定する必要があります。そのためには、Android Studio のメモリ プロファイラを使用します。

コードの問題の場所を特定したら、パフォーマンスが重要な箇所で割り当ての回数を減らしてみます。内側のループの外に移動するか、Factory ベースの割り当て構造に移動することを検討してください。

また、オブジェクト プールがユースケースに役立つかどうかを評価することも考えられます。オブジェクト プールを使用する場合、不要になったらオブジェクト インスタンスを捨てるのではなく、プールに放出します。次回、そのタイプのオブジェクト インスタンスが必要になったときは、そのオブジェクトを割り当てるのではなく、プールから取得できます。

ある状況でオブジェクト プールが適しているかどうかを判断するには、詳細なパフォーマンス評価が不可欠です。オブジェクト プールを使用するとパフォーマンスが低下する場合もあります。プールにより割り当てが回避されますが、それ以外のオーバーヘッドが生じます。たとえば、プールのメンテナンスには通常、同期という無視できないオーバーヘッドがあります。また、放出時にプールされたオブジェクト インスタンスを消去して(メモリリークを回避するため)、獲得時に初期化を行うと、オーバーヘッドがゼロにならないことがあります。さらに、プールに必要以上に多くのオブジェクト インスタンスを戻すことは、GC の負担となります。オブジェクト プールにより GC の呼び出し回数は減りますが、呼び出しごとに必要となる処理の量は増加します。これは、処理量が存続中の(到達可能な)バイト数に比例するためです。

メモリを大量に消費するリソースとライブラリを削除する

コード内の一部のリソースやライブラリが、知らないうちにメモリを大量に消費していることがあります。アプリの全体のサイズ(サードパーティのライブラリや埋め込みリソースを含む)がアプリのメモリ使用量に影響を及ぼすこともあります。冗長または不要な、あるいは肥大化したコンポーネント、リソース、ライブラリをコードから削除することにより、アプリのメモリ消費を改善できます。

APK の全体のサイズを小さくする

アプリの全体のサイズを小さくすることで、アプリのメモリ使用量を大幅に削減できます。ビットマップのサイズ、リソース、アニメーション フレーム、サードパーティ製ライブラリはすべて、アプリのサイズに影響する可能性があります。Android Studio と Android SDK には、リソースのサイズを小さくし、外部依存関係を縮小するのに役立つツールが複数用意されています。こうしたツールは、R8 コンパイルなどの最新のコード圧縮方法をサポートしています(Android Studio 3.3 以前では、R8 コンパイルの代わりに ProGuard を使用します)。

アプリ全体のサイズを縮小する方法の詳細については、アプリのサイズを縮小する方法に関するガイドをご覧ください。

依存性注入に Dagger 2 を使用する

依存性注入フレームワークを使用すると、記述するコードをシンプル化し、テストやその他の設定変更に役立つ適応性に優れた環境を構築できます。

アプリで依存性注入フレームワークを使用する場合は、Dagger 2 の使用を検討してください。 Dagger では、アプリのコードをスキャンするためにリフレクションを使用しません。 Dagger は静的で、コンパイル時に実行されるため、Android アプリでも使用できます。しかも、不必要なランタイム コストやメモリの使用は発生しません。

リフレクションを使用する他の依存性注入フレームワークは、アノテーションのコードをスキャンすることによってプロセスを初期化する傾向があります。このプロセスでは大量の CPU サイクルと RAM が必要になることがあり、アプリの起動時に顕著な遅延を発生させる可能性があります。

外部ライブラリの使用に注意する

外部ライブラリのコードはモバイル環境用に作成されていないことが多く、モバイル クライアントで使用すると効率性が低下する場合があります。外部ライブラリを使用する場合、そのライブラリをモバイル デバイス用に最適化する必要が生じることがあります。そのため、外部ライブラリを使用するかどうかを決定する前に、あらかじめその作業を計画し、コードサイズと RAM 使用量の観点からライブラリを分析してください。

モバイル デバイス向けの一部のライブラリでも、実装の違いによって問題が発生することがあります。たとえば、あるライブラリで lite 版のプロトコル バッファを使用し、別のライブラリで micro 版のプロトコル バッファを使用すると、アプリが 2 種類のプロトコル バッファの実装を持つことになります。ロギング、分析、画像の読み込みフレームワーク、キャッシュ保存などでも、複数の実装が生成されることがあります。

ProGuard を使用すると、適切なフラグの付いた API やリソースを削除できます。ただし、ライブラリの大規模な内部依存関係は削除できません。これらのライブラリで使用する機能では、低レベルの依存関係が必要になることがあります。これは、ライブラリから Activity サブクラスを使用する場合や(依存関係が広範に及ぶ傾向がある)、ライブラリでリフレクションを使用する場合(一般的な方法だが、ProGuard を手動で微調整して使用できるようにするのに時間がかかる)などには特に問題になります。

また、共有ライブラリにはさまざまな機能がありますが、そのうちの 1、2 個だけを使うために共有ライブラリを使用しないでください。使いもしない大量のコードやオーバーヘッドを含めるべきではありません。ライブラリを使用するかどうかを検討する際には、ニーズにぴったりマッチする実装を探してください。そうしないと、独自の実装を作成することになるかもしれません。