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

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

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

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

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

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

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

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 is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

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 is raised.
     */
    public void onTrimMemory(int level) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

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

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

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

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

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check 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 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 キャッシュ内で維持できるキャッシュ プロセスの数が減少し、アプリの切り替え効率が低下します。メモリの空き容量が少なく、実行中のすべてのサービスをホストするのに十分なプロセスを維持できない場合、システムでスラッシングが発生することもあります。

一般に、利用可能なメモリに対する需要が継続的に要求されるため、永続的なサービスの使用は避けてください。代わりに、WorkManager などを実装することをおすすめします。WorkManager を使用してバックグラウンド プロセスをスケジュール設定する方法の詳細については、永続処理をご覧ください。

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

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

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

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

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

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

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

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

詳細については、プロトコル バッファの README をご覧ください。

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

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

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

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

修正する前に、メモリ プロファイラを使用して、メモリチャーンがよく発生するコード内の場所を特定します。

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

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

パフォーマンスを入念に評価して、オブジェクト プールが特定の状況に適しているかどうかを判断してください。オブジェクト プールを使用するとパフォーマンスが低下する場合もあります。プールにより割り当てが回避されますが、それ以外のオーバーヘッドが生じます。たとえば、プールのメンテナンスには通常、同期という無視できないオーバーヘッドがあります。また、放出時にプールされたオブジェクト インスタンスを消去して(メモリリークを回避するため)、獲得時に初期化を行うと、オーバーヘッドがゼロにならないことがあります。

必要以上に多くのオブジェクト インスタンスをプールに保留すると、ガベージ コレクションの負荷も増大します。オブジェクト プールにより、ガベージ コレクションの呼び出し回数は減りますが、存続中の(到達可能な)バイト数に比例するため、呼び出しごとに必要な処理量は増加します。

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

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

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

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

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

依存性注入に Hilt または Dagger 2 を使用する

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

アプリで依存性注入フレームワークを使用する場合は、Hilt または Dagger の使用を検討してください。Hilt は、Dagger 上で実行される Android 用の依存関係インジェクション ライブラリです。Dagger は、アプリのコードをスキャンするためにリフレクションを使用しません。Android アプリで Dagger の静的なコンパイル時実装を使用すれば、不必要に実行時のコストやメモリを消費することはありません。

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

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

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

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

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

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