Unityメモリ最適化に関して、あう可能性のある問題ーーメモリ編

###キーワード
メモリ使用
メモリリーク
アセット冗長


##一、メモリ使用
**Q1: Unityのメモリ管理メカニズムでは、Reserved TotalとUsed Total間の関係は何ですか？**
>Reserved TotalとUsed Totalとは、Unityエンジンがメモリ方面の全体的な割り当てと全体的な使用量です。一般的に、エンジンがメモリを割り当てるとき、オペレーティングシステムに「取ったらすぐに使う」ではなく、最初に一定量の連続メモリを取得してから内部で使用します。メモリ残量が不足の時、エンジンはシステムに再び一定量の連続メモリを申し込んで使用します。だから、グラフから見えるのは、Reserved Totalのメモリ占有はUsed Totalより少しだけ大きく、傾向は基本的に同じであることがわかります。
![1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6654150f-62d1-66ce-f106-75860d0808d4.png)
注意：ほとんどのプラットフォームに対しては、Reserved Totalメモリ = Reserved Unityメモリ + GFXメモリ + FMODメモリ + Monoメモリ。[「パフォーマンス最適化、歩きは止まらないーーメモリ編」](https://qiita.com/UWATechnology/items/30e6dbfc07ccb7ad9403)を参照できます。

---
**Q2: UWAの助けを借りて、一つのReserved GFXメモリ占用を追跡しましたが、表示は比較的高かったです。どうすればこのメモリ占用を減らせられますか？**
>一般的に、Reserved GFXのメモリは、主にテクスチャとメッシュアセットです。テクスチャ形式を検査して、出来る限りにハードウェアがサポートしている圧縮テクスチャを使用してみます。メッシュアセットに対して、頂点の減少または頂点属性から始めることができます。

>さらに、テクスチャとメッシュアセット冗長（複数の同じアセット）であるか、リーク（たとえば、メインシティーの大きなテクスチャが戦闘シーンに表示される）であるかを検出することがより重要であり、回避する必要があります。セット冗長やメモリリークに関しては、開発者のみなさんは前の記事[「パフォーマンス最適化、歩きは止まらないーーメモリ編2」](https://qiita.com/UWATechnology/items/1b6a5a2f58cf5d556321)を参照できます。

---
**Q3: ProfilerにあるNot Savedはどういう意味ですか？**
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/0585d5df-9e1a-8567-baca-765fb5d1b419.png)
>ProfilerにあるNot Savedとは、プロジェクトにコードによって生成されるさまざまなアセットレコードを指します。上図のように、すべてのMeshは、NGUIプラグインがスクリプトを介して生成したUIインターフェイスMeshアセットであります。

---
**Q4: 図に示すように、EditorでProfilerにあるメモリの詳細情報を確認しますと、Used Totalに「Unity」があることを見つけました。これはどういう意味ですか？なぜこんなに大きいですか？**
![3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/5e4678a1-67b5-a7b4-2b6d-18b7fc6f8f81.jpeg)
>Editorで実行する時に、「Unityが大きい」は正常な状況です。その原因は、Editorでプロジェクトを実行する場合、エンジンにはアセットが占有するすべてのメモリが含まれ（GFXの一部のテクスチャとメッシュを除く）、同時に多くのサポート操作を行いて各種のゲーム実行データを記録します。一般的に、ゲーム実行中の実際のメモリ消費量を確認する場合は、リリースされたゲームのProfilerから直接観察することをお勧めします。Editorで観察されたメモリは実際より大きくなります。

---
**Q5：ProfilerのManagedHeap.UsedSizeとは何か、このパラメーターの意味は何ですか？ 重要ですか？**
>ManagedHeap.UsedSizeは、プロジェクトロジックコードが実行する時に申し込んだヒープメモリです。このオプションは、コードを最適化することによってのみ減らすことができます。一般的な最適化方法は下記のように。

>1、newの数を減らすために、変数を可能な限り再利用します。
2、String接続の代わりにStringBuilderを使用し、foreachの代わりにforを使用します。
3、局部変数または非常駐変数の場合は、可能な限りClassの代わりにStructを使用します。

>ManagedHeap.UsedSizeが大きすぎると、一方ではGCの時間に影響する可能性があり、他には、スクリプト内の不合理なGCAllocを反映する可能性もあります。

---
**Q6: System.ExecutableAndDllsは大量のメモリを占有し、増え続けます。これは正常ですか？**
>System.ExecutableAndDllsアイテムは、実行ファイルとコールされたライブラリ（物理、レンダリング、IO、およびその他のシステムライブラリ）の合計を示します。開発者たちはこのオプションの値に心配する必要はありません。その原因は、多くのアプリケーションがこれらのライブラリを共有しており、実際のプロジェクトに対するメモリの負荷は非常に小さく、ほとんど影響がありません。OSはこのメモリのためにゲームやアプリケーションを強制終了しません。

---
**Q7:下図のように、Profilerにこのようなコールされていないアセットを見られますが、まだメモリに残っていますか？**
![4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/9a48f02d-169e-0e93-5596-82b1854494cd.jpeg)
>そうです、Unity Profilerに見られるアセットは全てメモリに残っています。このアセットに対して、シーンを切り替える時にUnloadUnusedAssetsAPIをコールしたら解放できます。

---
**Q8: ゲーム内のMonoのメモリ占用を減らす方法はありますか？ゲームテーブルを読んだ後は60MB、他のゲームの設定テーブルを読んだ後は10MBくらいだったと思いますが、どうしてですか？**
>ゲームに入りばかり時に、Monoヒープメモリが60MBに達すると、ゲームプロジェクトは最初に非常に大きな配置テーブルをロードする可能性が高いです。これに対して、開発チームに配置ファイルの初期ロードを詳細に検査して、簡単化できるかどうかを確認することをお勧めします。できない場合には、配置ファイルを分解して要求次第でロードし、Monoメモリの上昇が速すぎる回数を減らします。

---
**Q9: Profiler.BeginSampleで統計したデータは、直接にMemoryで見たものと違います。前者は後者より大きいです、これはどうしてですか？ProfilerのAPIで取得した当該フレームのメモリコストは85.6MBですが、ヒープメモリでは62.9MBを示しています。**
![5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/38d4e430-9b7f-7f24-88d1-9b19c55ece2c.jpeg)
**Profiler中のデータ**
![6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/bc3d56ae-913f-92a4-8ecd-6fc2e2b6186c.jpeg)
>この状況は確かに遭う可能性があります。一つのフレームにこんな高いメモリを割り当てするとGC.Collectをトリガーする可能性もあります。Monoに表示する数値はGC後のMonoメモリの値です。

---
**Q10：通常の状況で、ゲームをプレイし続けると、Monoは増え続けますか？たとえば、一つのインターフェイスを頻繁に開く場合、インターフェイスにあるスクリプトは何かを作成し続けますと、Monoは増え続けますか？ パフォーマンスに影響しますか？**
>IL2CPP機能を開くアプリケーション以外、Monoは確かに低下しませんが、常に上昇するわけではありません。

>作成されたものは一つの容器にコールしますと、またはあるスクリプトの変数にコールしますと、この部分のヒープメモリを解放することはできません。ただし、容器または変数にコールしていない場合（例えば、一時的に一つのStringを作る）、この部分のヒープメモリはGCの時に解放します。（解放とは空きヒープメモリになることを指し、ヒープメモリの合計量は減少しません）。

>後者の場合、頻繁にオブジェクトをnewすることはヒープメモリを増やしませんが、GCコールの頻度が速くなさせるため、可能な限り回避する必要があります。

---
**Q11：下の図に示す関数では、毎回一つのList temp = list();を申し込んで6KBのデータを保存しますが、GC処理をしないと、この6KBは、GC処理をするまでに蓄積続けませんか？コール回数が多い場合、毎回少しずつコールすると、メモリ使用量も増加しますか？**
![7-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/e0606ca9-f639-32eb-6f64-2a95abf5c997.png)
>そうです、この6KBのヒープメモリは、Updateの実行によって常にメモリを割り当てし、蓄積されたヒープメモリはGCがトリガーされる時に破棄されます。一般的に、開発チームは頻繁にコール関数でヒープメモリの割り当て操作を出来る限りに回避します。

---
**Q12：メモリを最適化するとき、Unity Profilerが提供するデートとAndroidシステム(adb dumpsys meminfo、memtrackの影響もう考えた)のデータの差が少し大きいです（Profiler自体のメモリ使用量もう分析されています）。この部分のさはどうやって考えられますか？例えば、ビデオメモリコストの正確な統計、OSコストの統計など？**
>メモリの差が大きいのは正常です。一般的に、Profilerが統計したメモリは比較的一貫していますが、AndroidシステムがADBを介してフィードバックするPSSとPrivate Dirtyなどの値は大きく異なります。これは主にチップとOSの違いによるものです。具体的のAndroidメモリについては、Google Android OSの関連ドキュメントを直接確認することをお勧めします。

>Unity Profilerのフィードバックは、エンジンが使用する実際の物理メモリです。一般的に、Profilerを使用して、メモリが冗長であるかリークしているかを確認することをお勧めします。

---
**Q13：モンスターをプリロードした後、モンスターPSSが上昇と表示しています。モンスターを非表示にさせる後も下降しません。これは何が原因ですか？ ビデオメモリはアップしていますか？**
>モンスターを隠すだけでは、メモリは落ちません。非表示にすると、GameObjectの状態が変更されるだけで、メモリ内のObjectとアセットは削除されないためです。同時に、モンスターが事前にロードされている場合でも、Meshなど、一部のアセットは表示されたときにのみGPUに転送されるため、上記の問題が依然として存在する可能性があります。通常の状況では、ビデオメモリはすぐには削減されません。これはGraphics Driverによって管理されます。 Profilerが大きくなるかどうかを確認することをお勧めします。Profilerに問題がなく、PSSが大きくなり続けると、メモリリークが発生する可能性があります。

>この問題に関して、[「パフォーマンス最適化、歩きは止まらないーーメモリ編2」](https://qiita.com/UWATechnology/items/1b6a5a2f58cf5d556321)を参照できます。

---
**Q14：オープニングアニメーションを再生するためのUnity APIであるHandheld.PlayFullScreenMovieに関して、メモリの問題はありますか？たとえば、私のmp4アニメーションは20MBですが、このアニメーションはMonoヒープメモリを増やしますか？**
>AndroidでのPlayFullScreenMovieの実現は、実際にはAndroidのネイティブインターフェイスを介して直接再生されます。再生中にUnityの更新も停止します。ですから、理論的にこの部分のメモリはUnityに記録されず、Monoにも影響しません。

---
**Q15：テクスチャは常に2倍のメモリを占有しますが、これは私たち自身の問題ですか、それともUnityエンジンのメカニズムですか？**
>この状況には2つの理由があります。1つは、実機実行中にRead&Writeをオンにしていることです。もう1つの可能性はUnityのbugです。現在のUnity5.2.3release noteは次のとおりです。

>(735644) – OpenGL: Fixed texture memory usage reporting in profiler, was twice the actual size for most textures.

>開発者は自分の開発バージョンに注意を払う必要があります。5.2.3より前の同様の状況のプロジェクトを参照できます。

---
**Q16：スクリプトがGameObjectを引用している場合、シーンを変更すると、スクリプトとGameObjectの両方が失われます。ヒープメモリは生成されますか？**
>スクリプトがMonoBehaviourであり、シーンが切り替えられた後にハングしたGame Objectが解放される場合、このスクリプトオブジェクトによって引用されるヒープメモリはGC中に解放されます。ただし、例外が1つあります。ヒープメモリがStatic変数を介して引用されている場合、依然として解放できません。手動で引用を解放すべきです、例えば変数をNullにするや配列クリアなど。

##二、メモリリーク＆冗長
**Q1: テストしたところ、Aという名前のMonoBehaviourには、Bという名前のMonoBehaviourオブジェクトの引用を格納するための配列があることがわかりました。他のロジックがBオブジェクトが配置されているGameObjectをDestroyする後に、Aオブジェクトの配列に遍歴してプリントし、これら（Bの引用）はすべてNullであり、Inspectorパネルで見ればmissingであります。このとき、GCを実行しても、ヒープメモリは実際にはこれらのBオブジェクトを解放していません。Aオブジェクトの配列が空になり、GCをコール場合にのみ、これらのオブジェクトによって占有されていたメモリを解放できます。この現象は正常ですか？値がNullであるのに、まだ引用されており、GCによって解放できないのはなぜですか？**
>まずに、この現象は正常です。これは、UnityでNull検出に特別処理をすることが導きます。UnityでのMonoBehaviourオブジェクトはManaged Heapに（「シェル」として）存在するだけではなく、Nativeメモリにも対応する「エンティティ」があります。Destroyをコールすると、実際に解放されるのはこの「エンティティ」です。MonoBehaviourオブジェクトがNullであるかどうかを判断するとき、Unityは最初に「エンティティ」が破棄されたかどうかを検出し、破棄された場合はtrueを返しますが、この時点では、マネージドヒープの「シェル」は実際には引き続き引用されます。だから、オブジェクトのNull判断は「true」ですが、実際には引用されており、GCによって解放できない問題があります。

>詳細は公式blogで「UnityのNull判断」の説明を参照できます。
http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/

---
**Q2：複数のアセットの依存パッケージとして、フォントはゲーム内で複数回ロードされます。現在、一つの問題があります。AssetBundle Aアセットはこのフォントに依存し、Aをロードする時にフォントもロードされます。次にBアセットもこのフォントに依存しますが、Bをロードする時にフォントは再びロードしていません。この度、Bアセットでのフォントがなくなることがわかりました。ですから、アセットをロードする時に、Unityはメモリにアセットの依存関係があるかどうかを自動的に識別しますか？もしそうなら、Bがロードされたときにメモリ内のフォントが見つからないのはなぜですか？ ここに手動で何かをする必要がありますか？
同時に、依存パッケージアセットがbundle.m_AssetBundle.Unload(false)を行ったら、このパッケージに依存する他のアセットは引用できません。プロセスでは、読み込まれたAssetBundleごとに、ロードした後すぐにUnload(false)を行います。依存パッケージの場合、この手順を実行できますか？**
>Unityエンジンは、依存関係に基づいて依存アセットを自動的に検索しますが、依存AssetBundleファイルが存在する必要があることに注意してください。つまり、依存関係パッケージを後でまだ使用する場合は、アンロードしないでください。そうしないと、後続のAssetBundleがロードされた後、依存アセットが見つかりません。Unity 5.3より前のバージョンの場合、メモリを考慮して、開発チームはCreateFromFileまたはLoadFromCacheorDownloadを介してAssetBundleをロードできます。AssetBundle間の依存関係を保持しながら、Webstreamも生成させません。

---
**Q3：私たちのゲームを20分間プレイした後、Texture2Dのメモリが60MBを超え、繰り返しアセットもたくさんありますが、完全アンインストールされていないためですか？それとも、パッケージAssetBundleの依存関係の問題ですか？使用していますのはUGUIです。**
![8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/84d6c61f-ce2a-8943-193f-7efd70f46802.jpeg)
![9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/cdcffc54-fcda-00a4-71b5-ffca52a9426c.jpeg)
>2つの理由があります：

>1、AB依存関係のパッケージ化に問題があります。つまり、atlasは依存パッケージ化をしていません。
2、ロードとアンロードの管理上の問題があります。一度ロードした後に一つのContainerにインデックスされます。次に同じAssetBundleをロードした後に再びインデックスされます。これらが解放していない場合、問題を発生する可能性があります。



---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

UWA公式サイト：https://jp.uwa4d.com
UWA公式ブログ：https://blog.jp.uwa4d.com