Unity製iPhoneアプリのメモリ使用量の調査まとめ

はじめに

初めまして!ambrでUnityエンジニアをしているサックーと申します。

弊社のメタバースプラットフォームxambrにおいて、スマートフォン対応をすることとなり現在鋭意作業中となっています。 その中で課題となったのが使用可能メモリがこれまで対応していたデバイスよりも少ないことでした。

これまでの対応デバイスの中で最も搭載メモリが少ないのはMeta Quest2で、CPU・GPU共有のRAM合計は6GBです。 一方iPhoneにおいては搭載メモリがそれよりも少ない機種が多くありました。

そこでiPhone対応をしていくにあたって、システム部分で使用する容量とCG側で使用可能な容量を厳密に調べることが必要になったわけです。 この記事ではその調査の中で得た知見の概要をまとめたものになります。

使用環境

  • Unity 2021.1.28f1 URP
  • Memory Profiler 0.5.0-preview.1
  • Xcode 14.3
  • iOS16.4.1

1アプリが使用可能なメモリ容量

まず大前提として搭載したRAMの量に応じた1アプリで使用可能なメモリ容量がOSによって定められています。使用メモリがこの値を超えた瞬間に容赦なくアプリがクラッシュします。

使用可能メモリ量
「使用可能」の値はXcodeのDebugNavigatorで確認した数値で、実際にこの値を超えるとアプリが落ちることを確認しました。 搭載RAMが増えたからと言ってそれに比例して使用可能量が増えるわけではないようです。 もしかしたらiOSのバージョンによってこの値は変わるのかもしれません。

MemoryProfilerが消費するメモリ量

Unityでの開発において、メモリの使用量を計測するにはMemoryProfilerを使うと思いますが、実はその計測自体がメモリを消費し、計測後も残り続けるということが分かりました。今回使用した環境では1回の計測でおよそ111MBほど増えるようで、計測のたびに溜まっていくのでそのうちあふれてクラッシュしてしまいます。なので一度の実行においては基本的には一度だけSnapshotを撮るのがいいでしょう。また、MemoryProfilerを使えないのは開発上よくないため、この分の余裕を持ったメモリ設計をすることが推奨されます。

1度の実行で2回目に撮ったMemoryProfilerのSnapshot

XcodeのDebugNavigatorで見れるメモリ使用量とUnityのMemoryProfilerで見れるメモリ使用量の差

iPhone開発においてメモリの使用量を見る手段はUnityのMemoryProfilerだけではなく、XcodeのDebugNavigatorもありますよね。この二つで見れるメモリ使用量は異なっていて、それはそれぞれが監視している対象が異なるためです。下の例ではXcodeからは1.26GB消費していることになっていますが、MemoryProfilerからは366MBしか見えていません。

Xcodeから見たメモリ使用量
MemoryProfilerから見たメモリ消費量
MemoryProfilerが監視しているのは、CGアセットやC#スクリプトなどのUnity内のMagagedな領域だけであり、外部ライブラリなどに含まれる大体のdllやOS側のシステムで処理される動画などのNativeな要素は対象外となっています。Xcodeからはそれらを合わせたアプリ全体のメモリ使用量を監視しているため、上記制限はこちらを気にすることになります。

動画再生時のメモリ使用量

弊社のシステムではアプリ内で動画を再生する仕組みがあり、ローカルに全体をキャッシュして再生するダウンロード方式と、HLSを使用したストリーミング方式に対応しています。そのどちらにおいても再生時のメモリ使用量の増加はMemoryProfilerにはほとんど現れず、もっぱらXcode側に現れます。AVProを使っているのですが、処理としては動画をテクスチャに書き込み、マテリアルがそれを参照することで描画しているようです。 この背景を元に、動画の何の値がメモリ使用量に影響を与えているのかを確認するために、 * 解像度 * ビットレート * フレームレート * 動画長

を変えて調べてみました。 その結果メモリ使用量は解像度(ピクセル数)に比例し、他の要素の影響はほぼないことが分かりました。これは動画の処理時に非圧縮のピクセルデータを扱うためと思われます。具体的にはストリーミングの場合1ピクセル当たり37.5B程度消費すると計算でき、実際の解像度に当てはめると

  • 4096*2048の場合 300MB
  • 1920*1080の場合 74MB
  • 1024*576の場合 21MB

程度と算出され実測値もそれに近いものでした。この比例係数はダウンロードの方が大きく、バッファ量の違いが影響している可能性がありますが未検証です。仮に1ピクセル当たりの1フレーム分の値はRGBの3Bとすると、ストリーミングでは12フレーム程度バッファしている可能性があります。

描画解像度による影響

Unityでカメラを使用する場合、内部では描画解像度に合わせた解像度のRenderTextureが生成されています。そしてこのRenderTextureももちろんメモリを消費しています。iPhoneは機種によってディスプレイの解像度が異なるわけでして、高解像度の機種ではより多くのメモリを消費してしまいます。 1334x750のiPhone8では90.3MBだった消費量が、2436x1125のiPhoneXでは161.1MBに増えています。(個数も変わっていますが小さなサイズの物でした)

iPhone8
iPhoneX
しかしこれを緩和する方法としてFixedDPIという機能を使うことができます。これは実際の機器のDPIを無視して指定したDPI相当の解像度で描画を行う設定です。
Fixed DPIの設定
本来iPhoneXは458DPIですが、326と指定することで1722x800の解像度で描画されます。するとメモリ消費を105.3MBに抑えることができました。
DPIを326に指定した場合のiPhoneX
もちろんその分解像度が落ちているわけですが、オリジナルの解像度密度が3Dアプリケーションには過剰な節があり、視認性を確保できるUIデザインによって体験を損なわずにパフォーマンス確保を両立することは可能だと考えています。また、異なる機種間でも同じ解像感に揃えられるというメリットもあります。

CGに関するメモリ使用量

ようやく本来知りたかったCGで使うメモリ容量に来ました。シェーダーやパーティクルなどいくつかの要素がありますが、ここでは支配的な要素であるテクスチャとメッシュについて取り上げます。

テクスチャ

これは基本的に解像度と圧縮形式によって決まります。元のファイルのプロパティにかかわらず、Unity上でのImportSettingsによって決定されます。 テクスチャは膨大な量になりがちですが、Texture StreamingとMipmapの機能を使うことでメモリの消費量を制御することが可能です。

TextureStreamingの設定
QualitySettingsで設定できるMemoryBudgetという値は、Streaming Mipmapが有効でMipmapが存在するテクスチャが、合計で使用するメモリ容量上限を指定するものになります。この場合は100MBに指定しているので、シーン上にどれだけ対象のテクスチャがあったとしても、メモリ消費量はこれ以下に抑えられます。 仕組みとしては、カメラとの距離とBudgetの懐事情を勘案して、適切な解像度のMipmapがロードされるというものです。あまりに小さい値だと近場の画像もかなり低い解像度になってしまいますが、スマホの小さい画面では割と目立たなかったりするので相性がいいと感じました。 ただしTexture Streamingの対象外のテクスチャはBudgetを超えてメモリを消費するので注意が必要です。特にSpriteはMipmapを持てずに対象とできないので気を付けましょう。

メッシュ

メッシュにはTexture Streamingのような機能がないので、シーン上に存在するメッシュがすべてメモリに載ってしまいます。さらにアセットの情報からメモリ消費量を予測する計算式のようなものは見つけることができませんでした。MeshのAssetファイルに何やら頂点が持つデータ容量の記載がありますが、これがそのままメモリ消費量と一致するわけではありません。

MeshのAssetファイルのInspector
実際のメモリ消費量
そこで複数のメッシュについて頂点数とメモリ消費量の関係を調べたところある程度の法則が見えてきました。 式にすると頂点数×4×48 Bがおよそのメモリ消費量のようです。 48Bというのは1頂点当たりのUV0までのデータ量で、4の係数部分は実際に調べた中では3.58~4.2までの幅がありました。 この係数は1頂点が属するポリゴンの数が関係ありそうですが定かではないです。メッシュによってはUVを複数持っていますが、おおむねこの式が使えました。

まとめ

今回初めてここまでメモリ消費量と向き合いましたが、いろいろと面白い事実を知れて大変ではありましたが楽しかったです。Unityを使ったiPhoneアプリ開発で、メモリ消費量と戦う方の助けとなれば幸いです!