ゲームのメモリ使用量を効果的に最適化するには、まず Android プラットフォームがメモリを測定する方法と、システム テレメトリー、診断 API、プロファイリング ツールを使用する方法を理解する必要があります。このガイドでは、新しいプラットフォームのガイドラインに沿ってゲームのメモリ割り当てをモニタリング、キャプチャ、分析する方法について詳しく説明します。
RSS とスワップの指標について
ゲームのメモリ動作を効果的に分析してデバッグするには、Android プラットフォームがメモリの強制適用に使用する正確な技術指標を理解する必要があります。このテレメトリー パラメータがフィールドでどのように処理、モニタリングされるかについての詳細な背景情報については、Android Vitals - メモリ使用量(匿名 RSS + スワップ)のドキュメントをご覧ください。
1. 匿名 RSS(RssAnon)
Resident Set Size(RSS)は、デバイスの物理 RAM に保持されているプロセスによって占有されているメモリの部分を測定します。RSS は、ファイル バックアップ メモリと匿名メモリに分割されます。Android の適用指標は、匿名 RSS のみに焦点を当てています。
- 含まれるもの: ストレージ上の物理ファイルにリンクされていない、ゲームプロセスによって直接割り当てられたメモリページ。これらのページには、Java または Kotlin のヒープ、スレッド実行スタック、そして重要なネイティブ メモリ割り当て(カスタム C++ エンジン アロケータや、ネイティブ malloc または new を使用してリクエストされ、ゲームロジックによってダーティにされたメモリブロックなど)が含まれます。この指標の詳細については、Process Memory(RSS)ディクショナリをご覧ください。
- 重要性: ゲームエンジンは、物理演算、レンダリング、ロジックを処理するために、大規模なネイティブ メモリプールを使用します。これらのプールはファイルにバックアップされないため、完全に匿名 RSS に存在し、ゲームの物理フットプリントの大部分を占めます。
2. 非圧縮スワップ(VmSwap)
Android は、フラッシュ ストレージの摩耗とレイテンシの制約により、従来のディスクベースのスワップ領域をサポートしていません。代わりに、zRAM(非圧縮スワップ)を使用します。
- 仕組み: 物理 RAM の負荷が増加すると、カーネルのメモリ管理デーモンが非アクティブな匿名ページを圧縮し、物理 RAM の専用の非圧縮部分(zRAM)に移動します。
- 指標の計算: システムは、圧縮されていないサイズ(VmSwap)に基づいてこれを追跡し、ゲームの実際の物理メモリの需要を評価します。ゲームがメモリを割り当て、システムがそれを zRAM にスワップした場合でも、ゲームの合計メモリ フットプリントにカウントされます。
3. プロセスの状態
メモリ使用量は、Android Vitals でプロセス状態別に分類されます。ゲーム デベロッパーの場合、サードパーティの SDK やゲームが、ユーザーが認識するサービスやバックグラウンド サービスを予期せずトリガーすることもあります。
- 含まれるもの: フォアグラウンド、知覚可能、バックグラウンド、キャッシュ。
- 重要性: プロセスの状態によって、Android OS のメモリ管理に異なる影響があります。サードパーティの SDK が意図せずバックグラウンド タスクをトリガーした場合、ゲームが機密性の高いプロセス状態で実行されていることに気づかない可能性があります。
RunningAppProcessInfoを使用して、ゲームがバックグラウンドで実行されているかどうかをモニタリングします。
アプリケーション プログラミング インターフェース(API)
Android には、ゲームがメモリ不足に動的に対応し、実行時に詳細なメモリ診断をキャプチャできるシステム API が用意されています。
メモリ トリム イベントに応答する
システムは onTrimMemory を使用して、アプリがメモリ使用量を自主的に減らし、Low Memory Killer(LMK)による強制終了を回避して、他のアプリが使用できるようにメモリを解放するのに適したライフサイクル イベントをアプリに通知します。
システムがバックグラウンドでアプリを強制終了した場合、ユーザーは再開時にコールド スタートが遅いと感じます。バックグラウンドでのメモリ使用量を減らすことで、バックグラウンドでの終了を防ぐことができます。
トリム イベントに応答するときは、すぐに必要とされない再構築可能な大きなメモリ割り当てを解放します。
例:
TRIM_MEMORY_UI_HIDDENに応じて、ローカル ストレージからデコードされたキャッシュ ビットマップをトリムまたはパージします。
Kotlin
class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
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
public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
public void onTrimMemory(int level) {
switch (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.
}
}
}
}
ProfilingManager
Android 15(API レベル 35)で導入された ProfilingManager API を使用すると、アプリはプログラムで定義されたスナップショット(ヒープ プロファイル、システム トレース、Java ヒープダンプなど)をランタイムで直接キャプチャできます。
デベロッパーは、特定のシーンでキャプチャを手動でトリガーすることも、TRIGGER_TYPE_ANOMALY などの自動トリガーを登録して、ゲームプロセスがメモリ リミッターのしきい値を超えたときにキャプチャを自動的に起動することもできます。ただし、ゲーム デベロッパーは、最新のゲームエンジンの重要な制限事項を考慮する必要があります。
注: 最新のゲームエンジン(Unity や Unreal など)は、MAP_ANONYMOUS フラグを指定して mmap を使用し、カーネルから大規模な仮想メモリ ブロックを事前に割り当てることで、実行パフォーマンスを管理します。エンジンは、カスタム サブアロケータ(Unity のネイティブ メモリ マネージャーや Unreal の BinnedAllocators など)を使用して、メモリブロックを内部的に細分化して割り当てます。
ApplicationExitInfo
個々のプロセスのメモリ制限に違反したためにゲームがバックグラウンドで終了または強制終了された場合、標準の Java またはネイティブのクラッシュ ダンプ メカニズム(Firebase Crashlytics など)ではイベントが登録されません。これらの終了をプログラムでクエリしてログに記録するには、デベロッパーはゲームの起動時に ApplicationExitInfo API を活用する必要があります。
- 実装: 開始時に
ActivityManager.getHistoricalProcessExitReasons()を呼び出して、最近のセッションの終了理由を取得します。 - メモリ終了の主な理由:
REASON_LOW_MEMORY: システムのローメモリ キラー(LMK)によってプロセスが終了したことを示します。この終了は、デバイス全体のメモリ負荷が高く、OS が RAM を回収する必要がある場合に発生します。この終了理由は、ゲームのバックグラウンド フットプリントが大きすぎて、他のアプリと共存できないことを示しています。REASON_MEMORY_LIMITER(Android 17(API レベル 37)以上): プラットフォームのメモリ リミッターによって割り当てられた cgroup メモリ上限(RssAnon + VmSwap)を超えたため、プロセスが強制終了されたことを示します。この終了は、デバイスに十分な物理メモリが残っている場合でも発生する可能性があり、個々のプロセスの上限の直接的な違反を示しています。
利用可能なツールを使用する
開発と QA の段階で次のプラットフォーム ツールを使用して、ゲームのメモリ使用量を正確に測定します。
meminfo
このツールは、メモリの統計情報を収集して、割り当てられた PSS メモリの容量とメモリが使用されたカテゴリを表示します。
次のいずれかの方法で meminfo 統計情報を出力します。
adb shell dumpsys meminfo package-nameコマンドを使用する。- Android Debug API から
MemoryInfo呼び出しを使用する。
PrivateDirty 統計情報は、ディスクにページングできず他のプロセスと共有されていない、プロセス内の RAM の容量を示します。プロセスが強制終了されると、この容量の大部分をシステムで利用できるようになります。
メモリ トレースポイント
メモリ トレースポイントは、ゲームが使用している RSS メモリの量を追跡します。RSS メモリ使用量の計算は、PSS 使用量の計算よりはるかに高速です。計算が高速であるため、RSS はメモリサイズの変化をより詳細に示し、ピーク時のメモリ使用量をより正確に測定します。そのため、ゲームのメモリ不足を引き起こす可能性のあるピークに気づきやすくなります。
perfetto
Perfetto は、デバイスのパフォーマンスとメモリの情報を収集し、ウェブベースの UI に表示するためのツールのスイートです。任意の長いトレースをサポートしているため、RSS の経時的な変化を確認できます。オフライン処理用に生成したデータに対して SQL クエリを発行することもできます。システム トレース アプリから長いトレースを有効にします。トレースに対して memory:Memory カテゴリが有効になっていることを確認します。開発とテストにおけるカスタム メモリ インストゥルメンテーションには、(ベータ版)heapprofd API を使用することもできます。
RssAnon を検査して Perfetto でスワップする
ゲームの匿名メモリと zRAM スワップの影響を確認するには、ui.perfetto.dev のウェブベースの UI にトレースファイルを読み込み、メモリに関する詳細なケーススタディ用に設計された次の分析手法を使用します(詳しくは、Perfetto メモリ分析のケーススタディをご覧ください)。
1. タイムラインでのメモリ カウンタの可視化
- プロセスを見つける: ナビゲーション リストで、ゲームのパッケージ名またはプロセス名を検索します。
- トラック グループを開く: プロセス行をクリックしてスレッド トラックを開き、[メモリ] というサブグループを見つけます。
- トラックを分析します。
- mem.rss.anon(匿名 RSS): この折れ線グラフは、ゲームの管理対象外のメモリプールが占有している物理 RAM をリアルタイムで示します。シーンの読み込み、UI のポップアップ、ゲームプレイの切り替え中にこのタイムラインをモニタリングして、割り当てのピークが高いかどうかを確認します。
- mem.swap(圧縮スワップまたは VmSwap): このグラフは、zRAM に移動されたメモリブロックの圧縮前のサイズをプロットします。ゲームプレイと同時にスワップ アクティビティが高い場合は、ゲームがメモリ制限のあるデバイスで実行されており、システムがバックグラウンド アセットを積極的に圧縮していることを示します。
2. SQL クエリの実行(トレース プロセッサ)詳細なオフライン分析を行うには、Perfetto UI コンソール内で SQL クエリを直接実行するか、スタンドアロンのトレース プロセッサ Python ライブラリを使用して統計的なピークを計算します。
ピーク時の匿名 RSS 割り当てを確認します。
SELECT max(value) / 1024 / 1024 AS max_rss_anon_mb FROM counter JOIN counter_track ON counter.track_id = counter_track.id WHERE counter_track.name = 'mem.rss.anon' AND counter_track.upid IN ( SELECT upid FROM process WHERE name = 'your.game.package.name' );特定のタイムスタンプで RssAnon と VmSwap を関連付けます。
SELECT ts, track.name AS metric_type, value / 1024 / 1024 AS size_mb FROM counter JOIN counter_track track ON counter.track_id = track.id WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap') AND track.upid IN ( SELECT upid FROM process WHERE name = 'your.game.package.name' ) ORDER BY ts ASC;
Android Studio を使用してトレース ファイルを検証する方法について詳しくは、システム トレースを検証する: プロセスメモリ(RSS)をご覧ください。メモリ プロファイルのスクリプト作成の詳細については、ネイティブ割り当てを記録するをご覧ください。
heapprofd
heapprofd は、Perfetto の一部であるメモリ トラッキング ツールです。このツールは、malloc を使用してメモリが割り当てられた場所を示すことで、メモリリークの発見に役立ちます。heapprofd は Python スクリプトを使用して起動できます。このツールはオーバーヘッドが少ないため、他のツール(Malloc Debug など)のようにパフォーマンスに影響することはありません。
bugreport
bugreport は、ゲームがメモリ不足でクラッシュしたのかどうかを調べるロギングツールです。ツールから出力されるレポートは、logcat を使用するよりもはるかに詳細です。ゲームがメモリ不足でクラッシュしたのか、LMK によって強制終了されたのかが示されるため、メモリのデバッグに役立ちます。
詳細については、バグレポートのキャプチャと確認をご覧ください。
ゲームエンジン ツール
プラットフォーム レベルのログとシステム テレメトリーは、OS のしきい値とコンプライアンスを追跡するうえで不可欠ですが、ゲームエンジン固有のツールを使用すると、割り当てをゲーム オブジェクト、スクリプトの動作、アクティブなシーンの階層に直接関連付けることができます。
Unity
Unity Engine 環境では、Unity のネイティブ プロファイリング ツールとクラスを使用して、実行時に Android の匿名 RSS + スワップのメモリ使用量を高い信頼性で推定できます(通常、OS レベルの真の値と比較して 10% 未満の分散が表示されます)。
構成ルールやランタイム スクリプトなど、完全な手順については、Unity ツールでメモリをチェックする方法をご覧ください。
- Unity プロファイラ API: コア エンジン指標をクエリすることで、実行時にゲームのアンマネージド メモリ使用量をプログラムで概算できます。
- Profiler クラスを使用する:
Profiler.GetTotalReservedMemoryLong()とProfiler.GetMonoHeapSizeLong()の値を合計して、メモリ割り当ての合計を追跡します。 ProfilerRecorderクラスを使用する: メモリ カテゴリを動的にモニタリングします。信頼できるベースライン近似値を確立するには、Total Reserved Memory(リリースビルドの場合)を取得するか、Gfx Reserved Memory を差し引いて(開発ビルドの場合)、ファイル バックアップのグラフィック メモリ コンポーネントを取り除きます。
- Profiler クラスを使用する:
- Unity Memory Profiler: メモリリークをオフラインで特定してデバッグするには、メモリ スナップショットをキャプチャし、[All of Memory] セクションにある [Resident Memory on Device] グラフを調べます。おおよそのフットプリントを計算するには、[Untracked]、[Android Runtime]、[Native]、[Managed] の各カテゴリの合計を足します。
- zRAM の制限: メモリが不足している場合、Android カーネルは非アクティブなメモリページをスワップ領域(zRAM)に圧縮できます。Unity Memory Profiler は OS レベルのスワップ パラメータを検出できないため、メモリ使用量の多いシーンではフットプリントにわずかな差異が生じる可能性があります。見積もりを Perfetto と相互参照して、正確な値を確認します。