このページでは、アプリ内のメモリ使用量を事前に削減する方法について説明します。Android オペレーティング システムがメモリを管理する方法の詳細については、メモリ管理の概要をご覧ください。
ランダムアクセス メモリ(RAM)はどのソフトウェア開発環境でも貴重なリソースですが、物理メモリが制限されることが多いモバイル オペレーティング システムではさらに貴重になります。Android ランタイム(ART)と Dalvik 仮想マシンはどちらも、ガベージ コレクションを定期的に実行しますが、だからと言って、アプリがメモリの割り当てと解放を行うタイミングや場所を無視してよいわけではありません。引き続き、通常は静的なメンバー変数でのオブジェクト参照を維持することによって引き起こされるメモリリークが発生しないようにし、ライフサイクル コールバックで定義されているように適切なタイミングで Reference オブジェクトを解放する必要があります。
アプリのコードとリソースのフットプリントを削減する
コード内の一部のリソースやライブラリが、気付かないうちにメモリを消費することがあります。アプリの全体のサイズ(サードパーティのライブラリや埋め込みリソースを含む)がアプリのメモリ使用量に影響を及ぼすこともあります。冗長なコンポーネント、不要なコンポーネント、肥大化したコンポーネント、リソース、ライブラリをコードから削除することで、アプリのメモリ消費量を改善できます。
R8 を有効にしてアプリ全体のサイズを縮小する
コンパイルされたアプリケーション コードは、ランタイム メモリ フットプリントのアクティブな部分です。実行時に、すべてのクラス、メソッド、ライブラリの依存関係、文字列定数を RAM に読み込む必要があります。コンパイルされたコードベースが大きいほど、アプリが存在するために必要な物理 RAM が多くなります。
R8 を使用してアプリのメモリ使用量を削減できます。R8 は従来、APK サイズの縮小で知られていますが、ランタイム メモリ(RAM)にも直接的なプラスの影響があります。R8 はアプリのバイトコードを分析して、デッドコードの削除、冗長なクラスの統合、メソッドのインライン化、識別子の最小化を行います。APK から RAM に読み込むコンパイル済みバイトコードを減らすことで、アプリの全体的なベースライン メモリ使用量が減少します。また、クラス、メソッド、フィールド名を短い識別子に縮小することで、RAM のオーバーヘッドを直接削減できます。クラスの統合や大規模なメソッドのインライン化などの最適化により、コストのかかるランタイム ルックアップと割り当てパターンも置き換えられ、ヒープとスタック メモリが最適化されます。
保持ルールの概要
keep ルールは、最適化中に保持するコードの部分を R8 に指示する構成命令です。これにより、アプリが依存するコードが削除または縮小されるのを防ぎます。詳細については、保持ルールの概要をご覧ください。
keep ルールが適切に記述されていないと、R8 がコードベースの大部分を最適化できなくなります。広すぎる保持ルールは避け、次のベスト プラクティスに従ってください。
- 避けるべきグローバル ルール:
-dontoptimize: アプリ全体の最適化を完全に無効にし、実行可能ファイルが大きくなり、実行速度が遅くなります。-dontshrink: 未使用のコードとリソースの削除を防ぎます。-dontobfuscate: 名前の最小化を防ぎ、メモリの節約(特に大規模なアプリ)のメリットを逃します。
パッケージ全体のワイルドカードを避ける:
-keep class com.example.package.** { *; }のような広範なルールを使用すると、R8 はそのパッケージ内のすべてのクラス、フィールド、メソッドを保持します。これにより、R8 はそのパッケージ内のコードを削除、最適化、縮小できなくなります。デフォルトの R8 構成ファイルを使用する: 常に
proguard-android-optimize.txtを使用します。
保持ルールの作成の詳細については、保持ルールの概要をご覧ください。使用すべきパターンと避けるべきパターンについては、Keep ルールのベスト プラクティスをご覧ください。
R8 構成アナライザーは、R8 構成と、各保持ルールがアプリに与える影響に関する分析情報を提供します。最適化をブロックするルールを特定する方法について詳しくは、R8 構成アナライザーをご覧ください。
外部ライブラリの使用に注意する
外部ライブラリのコードは、モバイル環境用に作成されていないことが多く、モバイル クライアントでは非効率になる可能性があります。外部ライブラリを使用する場合、そのライブラリをモバイル デバイス用に最適化する必要が生じることがあります。この作業を事前に計画し、ライブラリを使用する前に、コードサイズと RAM 使用量の観点からライブラリを分析します。
モバイル デバイス向けの一部のライブラリでも、実装の違いによって問題が発生することがあります。たとえば、あるライブラリで lite 版のプロトコル バッファを使用し、別のライブラリで micro 版のプロトコル バッファを使用すると、アプリが 2 種類のプロトコル バッファの実装を持つことになります。ロギング、分析、画像の読み込みフレームワーク、キャッシュ保存などでも、複数の実装が生成されることがあります。
R8 を使用してアプリを最適化すると、依存関係から未使用のコードを削除できますが、その効果はライブラリの内部構成によって制限されることがよくあります。たとえば、広範な keep ルールやライブラリ内でのリフレクションの使用により、R8 がコードを縮小できず、メモリ フットプリントが大きくなることがあります。効率的なライブラリを選択するための戦略については、ライブラリを賢く選択するをご覧ください。
また、共有ライブラリにはさまざまな機能がありますが、そのうちの 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 ピクセルのビューで表示する場合、ビットマップでは 1,000, 000 ピクセルごとに 4 バイトが必要になり、合計で 4 MB のメモリが使用されます。
画像を最適に利用するためにできることはいくつかあります。たとえば、画像読み込みライブラリを使用すると、不要になったときにメモリを解放できます。画像を効率的に処理する方法については、ビットマップ画像を最適化するをご覧ください。
使用可能なメモリとメモリ使用量を監視する
修正する前に、アプリのメモリ使用量に関する問題を特定する必要があります。Android Studio のメモリ プロファイラを使用すると、以下の方法でメモリの問題を特定して診断できます。
- アプリによるメモリの割り当てを経時的に監視します。メモリ プロファイラでは、アプリのメモリ使用量、割り当てられている Java オブジェクトの数、ガベージ コレクションの実行タイミングがリアルタイムでグラフに表示されます。
- ガベージ コレクション イベントを開始して、アプリの実行中に Java ヒープのスナップショットを取得します。
- アプリのメモリ割り当てを記録し、割り当てられたオブジェクトをすべて検査して各割り当てのスタック トレースを表示し、Android Studio エディタで対応するコードに移動します。
Memory Profiler は LeakCanary リーク検出ライブラリとも統合されています。LeakCanary を使用すると、メモリリークの分析をテストデバイスから開発マシンに移動できるため、ワークフローを大幅に高速化できます。詳しくは、Android Studio のリリースノートをご覧ください。
本番環境アプリを実行しているユーザーからのデータに基づいてメモリの問題を診断するために使用できるツールは他にもあります。
- Android Vitals を使用して、Low Memory Kill(LMK)イベントをトラッキングします。
- Profiling Manager を使用して、メモリ不足エラーと、メモリリークが原因で発生する可能性のあるアプリの異常な動作をトラッキングします。
イベントに反応してメモリを解放する
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;
}
メモリ不足による強制終了をモニタリングする
ユーザーが認識できる Low Memory Kill(LMK)は、システムメモリが非常に少なくなったときに発生します。メモリが少なくなると、lmkd(ローメモリ キラーデーモン)は oom_adj_score に基づいてプロセスを終了します。キャッシュに保存されているアプリ、または関連する UI のないサービス(ジョブなど)を実行しているアプリは、スコアが最も高く、最初に終了します。メモリが極端に少ない状態が続くと、デーモンは oom_adj_score が 0 のプロセスからメモリを強制的に回収します。このスコアは可視アプリ用に予約されているため、終了すると、即座に非正常なプロセス終了が発生します。エンドユーザーには、アプリがクラッシュしたように見えます。多くの場合、標準のライフサイクル状態保存メカニズムがバイパスされ、ユーザーの進行状況が失われます。
フォアグラウンド プロセスの強制終了は、メモリの管理ミスを忠実に表す指標となるため、Android Vitals の主な焦点となっています。LMK 率が 1% を超えると、直ちに対処する必要があることを示しますが、LMK 率が低いからといって、必ずしも健全な状態であるとは限りません。ユーザーが認識した LMK 発生率が低い場合、LMK デーモンがバックグラウンドで実行中のプロセスを頻繁に強制終了している可能性があります。これにより、「ウォーム スタート」のパフォーマンスとマルチタスクの流動性が低下します。そのため、長期的な安定性とデバイスの状態を確保するために、現在の LMK スコアに関係なく、メモリのベスト プラクティスに準拠することをおすすめします。
ProfilingManager を使用してメモリの問題を追跡する
Android プラットフォームは、ProfilingManager を提供します。これは、設定したトリガーに基づいて本番環境でユーザーデータをキャプチャできる高度なオブザーバビリティ API です。これにより、再現が難しいメモリの問題を特定できます。
Android 17 で導入された 2 つの新しいトリガーは、メモリの問題を特定するのに特に役立ちます。
TRIGGER_TYPE_OOMは、アプリがOutOfMemoryErrorをスローしたことを示します。クラッシュの後にアプリが起動し、アプリがプロファイリング トリガーを登録したときにトリガーされます。TRIGGER_TYPE_ANOMALYは、システムがアプリの異常な動作を検出したときにトリガーされます。たとえば、メモリ使用量が過剰な場合などにトリガーされます。これは、アプリが過剰なメモリ使用を示した後に、システムが問題のあるプロセスを停止するアクションを実行する前にトリガーされます。たとえば、アプリが Android 17 で導入されたメモリ上限を超えると、システムがアプリを強制終了する前にTRIGGER_TYPE_ANOMALYがトリガーされます。
ProfilingManager を使用してトリガーをプログラムで登録および取得する方法については、トリガーベースのプロファイリングのドキュメントをご覧ください。
アプリ駆動型プロファイリングを使用して、トレースの開始点と終了点を手動で定義することもできます。メモリリークやメモリ使用量の過多が疑われる領域でヒープダンプやヒープ プロファイルをキャプチャする場合は、この方法を手動で行うことをおすすめします。
メモリ効率の高いコード構造を使用する
一部の Android 機能、Java クラス、コード構造は、他より多くのメモリを使用します。コード内でメモリ効率の高い別の方法を選択することで、アプリのメモリ使用量を最小限に抑えることができます。
サービスの利用頻度を抑える
必要でない場合はサービスを実行したままにしないことを強くおすすめします。不要なサービスを実行したままにしておくことは、Android アプリで起こりうるメモリ管理の最悪のミスの一つです。アプリがバックグラウンドで動作するためにサービスが必要な場合は、ジョブを実行する必要がない限り、サービスを実行したままにしないでください。タスクが完了したらサービスを停止します。そうしないと、メモリリークが発生する可能性があります。
サービスを開始すると、システムは常にそのサービスのプロセスを実行し続けようとします。この動作により、サービスによって使用される RAM が他のプロセスで使用できないままになるため、サービス プロセスの負荷が非常に大きくなります。そのため、システムが LRU キャッシュ内で維持できるキャッシュ プロセスの数が減少し、アプリの切り替え効率が低下します。メモリの空き容量が少なく、実行中のすべてのサービスをホストするのに十分なプロセスを維持できない場合、システムでスラッシングが発生することもあります。
一般に、利用可能なメモリに対する需要が継続的に要求されるため、永続的なサービスの使用は避けてください。代わりに、WorkManager などを実装することをおすすめします。WorkManager を使用してバックグラウンド プロセスをスケジュール設定する方法の詳細については、永続処理をご覧ください。
最適化されたデータコンテナを使用する
プログラミング言語が提供するクラスの一部は、モバイル デバイスでの使用には最適化されていません。たとえば、HashMap の一般的な実装では、マッピングごとに別々のエントリ オブジェクトが必要になるため、メモリ効率が低下する可能性があります。
Android フレームワークには、最適化されたデータコンテナがいくつか含まれています(SparseArray、SparseBooleanArray、LongSparseArray など)。たとえば、SparseArray クラスを使用すると、システムでキーと値の自動ボックス化(これにより、エントリごとに 1 つまたは 2 つのオブジェクトがさらに作成される)が不要になるため、効率性が向上します。
必要に応じて、いつでも生の配列に切り替えて、シンプルなデータ構造にすることができます。
コードの抽象化に注意する
抽象化はコードの柔軟性と保守性の向上に役立つため、プログラミングに関するおすすめの方法としてよく使用されます。ただし、抽象化では一般的に実行するコードが多くなります。アプリのコードとリソースのフットプリントを削減するで詳しく説明しているように、コンパイル済みのコードベースが大きいほど、アプリに必要な物理 RAM が増えます。抽象化によるメリットがあまりない場合は、抽象化の使用は避けてください。
シリアル化されたデータに対して lite 版のプロトコル バッファを使用する
プロトコル バッファ(protobufs)は、Google が設計した、構造化データをシリアル化するためのメカニズムです。言語やプラットフォームに依存せず、拡張することも可能です。XML に似ていますが、規模、処理速度、複雑さの点で XML より優れています。データにプロトコル バッファを使用する場合は、クライアントサイドのコードでは常に lite 版のプロトコル バッファを使用してください。通常のプロトコル バッファでは極めて冗長なコードが生成されるため、RAM 内のアプリのコード フットプリントが増加し(アプリのコード フットプリントを管理、最適化するを参照)、APK サイズの増大につながります。
詳細については、プロトコル バッファの README をご覧ください。
メモリリークに注意する
参照管理が適切でないと、オブジェクトが有用なライフスパンを超えて存続し、ガベージ コレクタがリークしたオブジェクトのメモリを再利用できなくなるメモリリークが発生する可能性があります。メモリリークを回避するには、ライフサイクルを認識する設計を実装します。
詳細については、メモリリークをご覧ください。
メモリチャーンを回避する
ガベージ コレクション イベントは、アプリのパフォーマンスに影響しません。ただし、ガベージ コレクタとアプリケーション スレッド間のやり取りが必要なため、短時間の間に多数のガベージ コレクション イベントが発生して、バッテリーがすぐに消費され、フレームのセットアップ時間がわずかに増加することがあります。システムがガベージ コレクションに費やす時間が多くなるほど、バッテリーの消耗が速くなります。
メモリチャーンが発生すると、多くの場合、ガベージ コレクション イベントが発生する回数が増加します。メモリチャーンは実際のところ、一定時間内に割り当てられた一時オブジェクトの数を表します。
たとえば、for ループ内で複数の一時オブジェクトが割り当てられることがあります。あるいは、ビューの onDraw() 関数内で新しい Paint オブジェクトや Bitmap オブジェクトが作成されることもあります。どちらの場合も、アプリによって大容量のオブジェクトが短時間で多数作成されます。これらのオブジェクトによって若い世代の使用可能なメモリがあっという間にすべて消費され、ガベージ コレクションが強制的に実行されます。
修正する前に、Memory Profilerを使用して、メモリチャーンがよく発生するコード内の場所を特定します。
コードの問題の場所を特定したら、パフォーマンスが重要な箇所で割り当ての回数を減らしてみます。内側のループの外に移動するか、Factory ベースの割り当て構造に移動することを検討してください。
また、オブジェクト プールがユースケースに役立つかどうかを評価することもできます。オブジェクト プールは、不要になったオブジェクト インスタンスを捨てるのではなく、プールに放出します。次回、そのタイプのオブジェクト インスタンスが必要になったときは、そのオブジェクトを割り当てるのではなく、プールから取得できます。
パフォーマンスを入念に評価して、オブジェクト プールが特定の状況に適しているかどうかを判断してください。オブジェクト プールを使用するとパフォーマンスが低下する場合もあります。プールにより割り当てが回避されますが、それ以外のオーバーヘッドが生じます。たとえば、プールのメンテナンスには通常、同期という無視できないオーバーヘッドがあります。また、放出時にプールされたオブジェクト インスタンスを消去して(メモリリークを回避するため)、獲得時に初期化を行うと、オーバーヘッドがゼロにならないことがあります。
必要以上に多くのオブジェクト インスタンスをプールに保留すると、ガベージ コレクションの負荷も増大します。オブジェクト プールにより、ガベージ コレクションの呼び出し回数は減りますが、呼び出しごとに必要な処理量は増加します。これは、処理量が存続中の(到達可能な)バイト数に比例するためです。