Android のメモリ管理を理解し、ゲームのメモリ使用量を測定するツールを設定したら、次はメモリ使用量を積極的に削減して最適化します。Android の厳しい制限内に収めることで、システムがゲームを終了するのを防ぎ、起動時間を短縮し、すべてのデバイスでゲームが正常に動作するようにします。
このガイドでは、ゲームのメモリ使用量を削減するための実践的な手法を紹介します。特に、アセットレベルの最適化、エンジン固有の構成、メモリ管理のベスト プラクティスに焦点を当てています。
Unity でメモリを削減する
Unity のアーキテクチャ設計により、ネイティブ ブロック アロケータとマネージド ヒープが拡張されると、アセットが解放された後でも、エンジンはそれらのメモリページをオペレーティング システム(OS)にすぐに返すのではなく、再利用のために保持する傾向があります。具体的には、仮想アドレス空間(予約済みメモリ)はプロセスの存続期間中予約されたままになり、物理メモリ(RSS)は、複数のガベージ コレクション(GC)とトリミング サイクルが発生するまで、すぐに再利用されません。そのため、一時的なメモリのピークにより、実際の使用量が減少した後も、常駐メモリが長時間にわたって膨張した状態になることがあります。この動作により、ローエンド デバイスでメモリ不足(OOM)クラッシュが発生するリスクが高まり、ランタイム全体の安定性が低下します。
そのため、Unity のメモリ最適化は、これらのエンジン動作に合わせて調整された 3 つのコア ピラーを通じてアプローチする必要があります。
- アクティブなメモリ使用量を制御して、ピーク時のメモリ使用量の急増を未然に防ぎます。
- テクスチャ形式とシェーダー バリアントを管理して、不要なアセットとネイティブ リソースがインスタンス化されないようにします。
- マネージド ヒープでの不要な割り当てを排除し、GC の頻度とヒープの拡張を最小限に抑えるように、ランタイム コード構造をリファクタリングします。
詳細については、Unity のメモリ最適化をご覧ください。
Unreal Engine でメモリを削減する
Unreal Engine では、高忠実度のレンダリング パイプラインと複雑なオブジェクト依存関係グラフにより、匿名 RSS とファイル バックアップ メモリの両方のプレッシャーが大幅に高まる可能性があります。特に、ハード参照と深いブループリント継承階層に依存すると、未使用の接続されたアセットがメモリに読み込まれます。さらに、シェーダーの過剰な順列、最適化されていないテクスチャ ストリーミング プール、圧縮されていない ELF 再配置テーブルは、ベースラインのメモリ フットプリントを大きくし、低メモリ キラー(LMK)による終了のリスクを高めます。
そのため、Unreal Engine のメモリ最適化は、これらのエンジン動作に合わせて調整された 3 つのコア ピラーを通じてアプローチする必要があります。
- データとロジックを分離し、ハード参照または強い参照をソフト参照または弱い参照に置き換えます。
- 使用されていないモバイル ライティング機能と順列オプションを削除して、パイプライン状態オブジェクト(PSO)と冗長なレンダー ターゲットを最小限に抑え、ASTC 圧縮を適用して、デバイス プロファイルでテクスチャ ストリーミング プールをカスタマイズします。
- RELR と APS 再配置テーブル圧縮を有効にして、ELF バイナリのサイズを縮小し、ランタイムの物理メモリ使用量を削減します。
詳細については、Unreal のメモリ最適化をご覧ください。
マルチプロセス最適化
キャッシュに保存されているプロセスのメモリ使用量は、アクティブなアプリに影響しないため、メモリ上限の計算から除外されます。サービスを分離された別のプロセスで実行すると、メインプロセスが可能な限り迅速にキャッシュ状態に移行し、ゲームの健全性が向上します。
詳しくは、プロセス状態とメモリを追跡する方法、Unity でサービス プロセスを分離する方法、Unreal でサービス プロセスを分離する方法をご覧ください。
ユーザーが認識するサービスでのメモリ使用量を削減
ゲームでは、大容量のダウンロードの完了やバックグラウンドの音声チャット システムなどのユースケースで、ユーザーが認識するサービスでロジックを実行する必要がある場合があります。これらの戦略は、このようなシナリオでメモリ使用量を管理し、削減するのに役立ちます。
サイズの大きなダウンロードに関する戦略
これらの戦略は、ユーザーがゲームを最小化した後も継続したい大規模なダウンロードに適用される可能性があります。
1. ダウンロード プロセスを分離する
内容: メモリ アロケータがメモリプール ページを保持し、配列を解放した後も匿名 RSS メモリを人為的に高く保つ可能性があるため、別のプロセスでダウンロードを実行することで、アプリが使用していないメモリを OS がすぐに再利用できるようにします。サービスとプロセスを明示的に終了または終了すると、メモリは OS プールに戻り、メインプロセスは影響を受けません。
Unity の場合: カスタム マニフェストで
android:process=":downloader"などのプロセスで宣言されたネイティブ AndroidServiceにダウンロードをオフロードし、Unity のAndroidJavaClassJNI で呼び出します。ダウンロードが完了したら、必ずプロセスを終了してください。詳しくは、Unity で別のプロセスで知覚可能なサービスを実行するをご覧ください。Unreal の場合: Unreal Plugin Language を使用して
android:process=":downloader"のようなプロセスでカスタム AndroidServiceを宣言し、C++ JNI でトリガーします。ダウンロードが完了したら、必ずプロセスを終了してください。詳しくは、Unreal で別のプロセスで知覚可能なサービスを実行するをご覧ください。ネイティブ Android の場合:
AndroidManifestでServiceを宣言します(プロセスはandroid:process=":downloader"など)。この分離されたプロセスでダウンロードを実行し、ダウンロードが完了したらProcess.killProcess(Process.myPid())を呼び出します。
メリット: メモリが保持される期間を短縮し、大きなメインプロセスで使用されるメモリを解放しながらダウンロードを継続できます。
2. ダウンロードをディスクに直接ストリーミングする
内容: ネットワーク レスポンスを大きな配列に蓄積してから書き込むのではなく、小さな固定サイズの再利用可能なバッファを使用して、ネットワーク ソケットからディスクにデータを直接ストリーミングします。
Unity の場合: アセット バンドルや大きなファイルには
DownloadHandlerBufferを使用しないでください。ファイルサイズ(匿名 RSS メモリ)と同等のネイティブ メモリバッファが割り当てられるためです。代わりに、DownloadHandlerFileを使用して、バックグラウンド スレッドでバイトをディスクにネイティブにストリーミングします。Unreal の場合:
IHttpRequestからのペイロードをTArray<uint8>に追加するのではなく、SetResponseBodyReceiveStream()を使用して、受信したデータチャンクをFArchive(Unreal のファイル マネージャーを使用するファイル バックアップ アーカイブ)に直接パイプします。ネイティブ Android の場合: HTTP レスポンスで
.readBytes()または.string()を呼び出すのではなく、プールされたバッファを使用してInputStreamをFileOutputStreamにパイプします。
メリット: メモリ使用量のピークを削減する
3. ストリーム ダウンロード ファイルの解凍
内容: ダウンロードが圧縮されている場合、ファイルをダウンロードして RAM に読み込み、抽出するのではなく、ネットワーク入力ストリームを ZipInputStream などのストリーミング解凍ツールでラップします。
メリット: メモリ使用量のピークを削減する
4. OS に委任する
内容: バックグラウンド メモリの管理を完全に回避するには、Android のネイティブ API に処理を委任します。
WorkManagerは、OS レベルのJobSchedulerの最新の推奨ラッパーです。Android 14 以降では、WorkManagerはユーザーがトリガーしたダウンロードをユーザーが開始したデータ転送(UIDT)ジョブとして自動的に処理します。これはアプリのプロセス内で実行されるため、メモリ使用量を最小限に抑えるには、ディスクに直接ストリーミングする必要があります。UIDT は、システム リソースが制限された場合に OS がダウンロードを適切に一時停止して再開できるようにすることで、メモリ不足によるクラッシュからアプリを保護します。DownloadManagerは別のシステム プロセスで実行され、ダウンロードのメモリ使用量をアプリに割り当てません。ファイルがダウンロードされて準備が整うと、アプリはブロードキャスト通知を受け取ります。
メリット: WorkManager はメモリ不足のシナリオに対処するのに役立ち、DownloadManager はアプリのメモリ使用量を削減します。
補助サービスの戦略
これらの戦略は、バックグラウンド音声チャットなど、メインのゲームプロセスと並行してゲームが実行する補助サービスに適用される場合があります。
1. プロセスを分離する
内容: 機能(ボイスチャット ソリューションなど)をメインのゲームエンジンから切り離します。たとえば、マイクのキャプチャとネットワーク ストリーミングを、別のプロセスに割り当てられた Android フォアグラウンド サービス内で実行できます(マニフェストで宣言します。たとえば android:process=":voice" を使用します)。
メリット: アプリが最小化されると、重いメインゲーム プロセスは優先度の低いキャッシュ状態に移行し、軽量の補助サービスはユーザーが認識できるサービス状態を維持します。
2. 未使用のインプロセス メモリをトリムする
内容: 補助サービスがゲームエンジンに深く統合されていて分離できない場合は、ゲームがバックグラウンドに移行または最小化されたら、できるだけ早くプロセスから負荷を軽減します。テクスチャ キャッシュのフラッシュ、不要なシーンのアンロード、エンジン チックとレンダリング レートの 0 への引き下げ、ガベージ コレクションの明示的な呼び出しを検討してください。
Unity の場合:
OnApplicationPause()が発生したときにこのトリミングを行います。Resources.UnloadUnusedAssets()をご覧ください。Unreal の場合: トリミング ロジックを
ApplicationWillEnterBackgroundDelegateデリゲートにバインドします。ネイティブ Android の場合: 必要に応じて、
onPause()またはonStop()でトリミングを行います。OS は、メモリ使用量の多いプロセスを強制終了する前に、onTrimMemory()の実装を呼び出そうとします。
メリット: ゲームがフォアグラウンドにないときに不要なメモリ使用量を削減します。