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

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

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

アプリのコードとリソースのフットプリントを削減する

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

R8 を有効にしてアプリ全体のサイズを縮小する

コンパイル済みのアプリケーション コードは、ランタイム メモリ フットプリントのアクティブな部分です。実行時には、すべてのクラス、メソッド、ライブラリの依存関係、文字列定数を RAM に読み込む必要があります。コンパイル済みのコードベースが大きいほど、アプリに必要な物理 RAM の量が多くなります。

R8 を使用すると、アプリのメモリ使用量を削減できますR8 は従来、APK サイズの縮小に使用されていましたが、ランタイムメモリ(RAM)に直接的なプラスの影響を与えます。R8 はアプリのバイトコードを分析して、デッドコードの削除、冗長なクラスのマージ、インライン メソッド、識別子の最小化を行います。APK から RAM に読み込むコンパイル済みバイトコードを減らすことで、アプリのベースライン メモリ使用量全体が減少します。また、クラス名、メソッド名、フィールド名を短い識別子に最小化すると、RAM のオーバーヘッドが直接削減されます。 クラスのマージや広範なメソッドのインライン化などの最適化により、コストのかかるランタイム ルックアップと割り当てパターンが置き換えられ、ヒープメモリとスタック メモリが最適化されます。

keep ルールについて

keep ルールは、最適化中にコードのどの部分を保持するかを R8 に指示する構成手順です。これにより、アプリが依存するコードが削除または最小化されるのを防ぎます。詳細については、keep ルールの概要をご覧ください。

keep ルールの記述が不適切な場合、R8 はコードベースの大部分を最適化できません。広すぎる keep ルールは避け、次のベスト プラクティスに従ってください。

  • 避けるべきグローバル ルール:
    • -dontoptimize: アプリ全体の最適化を完全に無効にし、実行可能ファイルのサイズが大きくなり、速度が低下します。
    • -dontshrink: 未使用のコードとリソースの削除を防ぎます。
    • -dontobfuscate: 名前の最小化を防ぎ、貴重なメモリの節約(特に大規模なアプリの場合)を逃します。
  • パッケージ全体のワイルドカードを避ける: -keep class com.example.package.** { *; } などの広範なルールを使用すると、R8 はそのパッケージ内のすべてのクラス、フィールド、 メソッドを保持します。これにより、R8 はそのパッケージ内のコードの削除、最適化、最小化を完全に停止します。

  • デフォルトの R8 構成ファイルを使用する: 常に proguard-android-optimize.txt を使用します。

keep ルールの記述について詳しくは、keep ルールの概要をご覧ください。 使用すべきパターンと避けるべきパターンについては、keep ルールのベスト プラクティスをご覧ください。

R8 Configuration Analyzer は、R8 構成と 各 keep ルールがアプリに与える影響に関する分析情報を提供します。最適化をブロックするルールを特定する方法について詳しくは、R8 Configuration Analyzer をご覧ください。

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

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

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

R8 を使用してアプリを最適化すると、依存関係から 未使用のコードを削除できますが、その効果はライブラリの内部 構成によって制限されることがよくあります。たとえば、広範な keep ルールやライブラリ内でのリフレクションの使用により、R8 がコードを縮小できず、メモリ フットプリントが大きくなることがあります。効率的なライブラリを選択する戦略については、Choose libraries wiselyをご覧ください。

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

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

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

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

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

依存性注入を使用する場合は、オブジェクトが適切にスコープ設定されていることを確認して、メモリリークが発生しないように注意してください。オブジェクトを誤ったライフサイクルにバインドして必要以上に長く保持すると、メモリリークが発生する可能性があります。

画像の読み込みを意図的に行う

通常、グラフィック ビットマップは、アプリのメモリに存在する最も一般的なオブジェクトです。JPEG などの圧縮ファイルを使用している場合でも、画面に表示するには、ファイルを解凍して圧縮されていないビットマップにする必要があります。圧縮された小さな画像ファイルが、非常に大きなビットマップに展開されることがあります。

たとえば、ほとんどのビットマップは ARGB_8888 構成を使用します。つまり、各ピクセルに 4 バイトのメモリが必要です。赤、緑、青、アルファ(透明度)にそれぞれ 1 バイトです。100 KB の JPEG を 1,000×1,000 ピクセルのビューに表示する場合、ビットマップには 100 万ピクセルごとに 4 バイトが必要となり、合計 4 MB のメモリが必要になります。

画像の利用を最適化するためにできることはいくつかあります。たとえば、画像読み込みライブラリを使用すると、不要になったときにメモリを解放できます。 画像を効率的に処理する方法については、ビットマップ画像の 最適化をご覧ください。

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

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

メモリ プロファイラは、LeakCanary リーク検出 ライブラリとも統合されています。LeakCanary を使用すると、メモリリークの分析をテストデバイスから開発マシンに移動できるため、ワークフローを大幅に高速化できます。詳しくは、Android Studio のリリースノートをご覧ください。

本番環境アプリを実行しているユーザーからのデータに基づいてメモリの問題を診断するために使用できるツールは他にもあります。

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

Android では、メモリ管理の概要で説明されているように、重要なタスクにメモリを解放するために、必要に応じてアプリからメモリを再利用したりアプリを完全に停止したりできます。システムメモリのバランスを細かく調整して、システムがアプリプロセスを強制終了せずに済むようにするには、ComponentCallbacks2 インターフェースを Activityクラスに実装します。用意されている onTrimMemory() コールバック メソッドは、アプリがメモリ使用量を自主的に削減するのに適したライフサイクルまたはメモリ関連のイベントをアプリに通知します。 メモリを解放すると、ローメモリキラーによってアプリが強制終了される頻度が減る可能性があります。

onTrimMemory() の実装では、TRIM_MEMORY_UI_HIDDEN イベントと TRIM_MEMORY_BACKGROUND イベントにのみ焦点を当てる必要があります。(Android 14 以降では、他のレガシー定数の通知は配信されなくなりました。これらの定数は Android 15 で正式に非推奨になりました)。

  • TRIM_MEMORY_UI_HIDDEN: このシグナルは、アプリの UI がユーザーのビューから移行したことを示します。この移行により、ビットマップ、動画再生バッファ、複雑なアニメーション リソースなど、UI に厳密に結び付けられた大量のメモリ割り当てを解放できます。

  • TRIM_MEMORY_BACKGROUND: このシグナルは、プロセスがバックグラウンドに存在し、システムのグローバル メモリのニーズを満たすために強制終了の候補になっていることを示します。プロセスがキャッシュされた状態を維持する時間を延長し、アプリのコールド スタートの回数を減らすには、ユーザーがセッションを再開したときに簡単に再構築できるリソースを積極的に解放する必要があります。

このコードサンプルは、さまざまなメモリ関連のイベントに応答するように 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;
}

ローメモリ キルをモニタリングする

ユーザーに表示されるローメモリ キル(LMK)は、システムメモリが非常に少なくなったときに発生します。メモリが少ない場合、 lmkd(ローメモリキラー デーモン)はoom_adj_scoreに基づいてプロセスを強制終了します。キャッシュに保存されているアプリや、関連する UI のないサービス(ジョブなど)を実行しているアプリはスコアが最も高く、最初に強制終了されます。メモリが非常に少ない状態が続くと、デーモンは oom_adj_score が 0 のプロセスからメモリを再利用せざるを得なくなります。 このスコアは表示されるアプリ用に予約されているため、強制終了すると、プロセスがすぐに強制終了されます。エンドユーザーには、アプリがクラッシュしたように見えます。多くの場合、標準のライフサイクル状態保存メカニズムがバイパスされ、ユーザーの進行状況が失われます。

フォアグラウンド プロセスの強制終了は、メモリの誤管理の忠実度の高いプロキシとして機能するため、Android Vitals で重点的に取り上げられています。LMK 率が 1% を超えている場合は、直ちに対処する必要がありますが、低いレートが必ずしも健全性を示すとは限りません。ユーザーが認識した LMK 率が低い場合、LMK デーモンがバックグラウンドでプロセスを頻繁に強制終了している可能性があります。これにより、「ウォーム スタート」のパフォーマンスとマルチタスクの流動性が低下します。 そのため、現在の LMK スコアに関係なく、メモリのベストプラクティスに従って、長期的な安定性とデバイスの健全性を確保することをおすすめします。

ProfilingManager を使用してメモリの問題をトラッキングする

Android プラットフォームには、設定したトリガーに基づいて本番環境でユーザーデータをキャプチャできる 高度なオブザーバビリティ API である ProfilingManager が用意されています。これにより、再現が難しいメモリの問題を特定できます。

Android 17 で導入された 2 つの新しいトリガーは、メモリの問題を特定するのに特に役立ちます。

ProfilingManager を使用してトリガーをプログラムで登録 して取得する方法について詳しくは、トリガーベースの プロファイリング のドキュメントをご覧ください。

アプリ主導のプロファイリングを使用して、開始トレース ポイントと終了トレース ポイントを手動で定義することもできます。メモリリークや過剰なメモリ使用量が発生する可能性があると思われる領域で、ヒープダンプまたはヒープ プロファイルをキャプチャすることをおすすめします。

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

一部の 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オブジェクトを作成することがあります。どちらの場合も、アプリによって大容量のオブジェクトが短時間で多数作成されます。これらのオブジェクトによって若い世代の使用可能なメモリがあっという間にすべて消費され、ガベージ コレクションが強制的に実行されます。

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

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

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

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

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