Unity4.6での話です。
Unity5では未検証。
DestroyしたオブジェクトのMaterialがリークしていた
// いろいろな処理
// プレハブから新たなGameObjectを生成
var newGameObject = Instantiate(prefabHoge) as GameObject;
// いろいろな処理
// 用済みのGameObjectを破棄
Destroy(newGameObject);
// いろいろな処理
上記のように、必要に応じてプレハブからInstantiateして、用が済んだらDestroyするような処理が繰り返されるUnityアプリのテストをしていたところ、長時間動作しているときにメモリ使用量が増え続ける現象に遭遇。
UnityエディタのProfilerとにらめっこして、いったいどこにメモリを喰われているのかを検証していたところ、犯人はMaterial。
どうやらInstantiateで生成されたオブジェクトのRendererが持っているMaterial(Clone)が、Rendererが破棄されたあとも残り続けている模様。
Profilerでの計測結果
「Prefabを大量にInstantiate、その後すべてDestroy、またInstantiate……」
これを繰り返し、メモリの使用量をProfilerで計測してみた。
現状のメモリ使用量を計測
開始時との差分(MB) | Unity | Mono | Materials |
---|---|---|---|
開始時 | 0.0 | 0.0 | 0.0 |
Instantiate | 8.7 | 4.6 | 2.5 |
Destroy | 8.5 | 6.7 | 3.1 |
Instantiate | 15.2 | 3.0 | 5.2 |
Destroy | 14.6 | 6.2 | 5.7 |
Instantiate | 21.4 | 3.7 | 7.7 |
Destroy | 20.8 | 5.7 | 8.2 |
UnityとMaterialの使用量がDestroy時に解放されず、繰り返すごとに増えていく。
次に、Resource.UnloadUnusedAssetsとGC.Collectで明示的に不使用アセットを解放してみることにした。
不使用アセットの強制解放
開始時との差分(MB) | Unity | Materials | Materials(数) |
---|---|---|---|
開始時 | 0.0 | 0.0 | 0 |
Instantiate | 7.4 | 1.9 | 1359 |
Destroy | 6.0 | 2.4 | 1729 |
Unload & GC | -0.2 | 0.0 | 58 |
Instantiate | 7.5 | 2.0 | 1346 |
Destroy | 6.0 | 2.5 | 1865 |
Unload & GC | 0.0 | 0.0 | 58 |
Instantiate | 7.2 | 2.0 | 1388 |
Destroy | 6.5 | 2.5 | 1786 |
Unload & GC | 0.9 | 0.2 | 141 |
Resource.UnloadUnusedAssetsとGC.Collectが走ったタイミングでしっかり解放されている。
※Destroy時にMaterial数が増えているのは、破棄時の演出処理によるもの。
解放はされるものの、この方法だと解放のたびに画面の更新が一瞬止まってしまう。
そこで、リークしているMaterialを明示的にDestroyしてみた。
Renderer.materialsをDestroyImmediate
開始時との差分(MB) | Unity | Materials | Materials(数) |
---|---|---|---|
開始時 | 0.0 | 0.0 | 0 |
Instantiate | 7.9 | 1.9 | 1228 |
Destroy | 2.2 | 0.4 | 409 |
Instantiate | 9.0 | 2.2 | 1541 |
Destroy | 3.2 | 0.8 | 796 |
Instantiate | 10.5 | 2.7 | 1996 |
Destroy | 4.2 | 1.3 | 1196 |
Renderer.materialsで使っているMaterialを、OnDestroy時にDestroyImmediateした結果。
確かにメモリ使用量が減っていて、解放されているようだが、まだMaterialがリークしている。
ちゃんと消しているはずなのに……と思ったら、Renderer以外でもMaterialが使用されているのに気付いた。
実は今回の現象で使用していたPrefabは、演出のためにParticleSystemが使用されていた。
そこで、Rendererに加えて、ParticleSystem.renderer.materialsもDestroyImmediateしてみた。
ParticleSystem.renderer.materialsもDestroyImmediate
開始時との差分(MB) | Unity | Materials | Materials(数) |
---|---|---|---|
開始時 | 0.0 | 0.0 | 0 |
Instantiate | 7.0 | 1.8 | 1175 |
Destroy | 0.3 | 0.0 | 25 |
Instantiate | 6.8 | 1.7 | 1115 |
Destroy | 0.3 | 0.0 | 12 |
Instantiate | 7.3 | 1.8 | 1170 |
Destroy | 0.4 | 0.0 | 12 |
間違いなく解放されているようだ。
これでMaterialのリークは起きなくなった。
Materialの自動解放
OnDestroy時に、自身が使用していたMaterialを明示的に破棄すれば、リークは起こらないようだ。
しかし、毎回OnDestroyに解放処理を書くのは面倒なので、自動解放用のスクリプトを書いた。
using UnityEngine;
/// <summary>
/// 自身の破棄時に、自動的にMaterialを破棄する
/// </summary>
public class AutoDestroyMaterials : MonoBehaviour {
#region Parameters
// 重複登録防止のためのルートフラグ
[System.NonSerialized] public bool IsRoot = true;
#endregion
void Start(){
if(IsRoot){
// 配下のレンダラすべてに追加
foreach(var r in this.GetComponentsInChildren<Renderer>()){
r.gameObject.AddComponent<AutoDestroyMaterials>().IsRoot = false;
}
// 配下のパーティクルシステムすべてに追加
foreach(var p in this.GetComponentsInChildren<ParticleSystem>()){
p.gameObject.AddComponent<AutoDestroyMaterials>().IsRoot = false;
}
}
}
void OnDestroy(){
// レンダラのマテリアルを破棄(パーティクルシステムのレンダラも含まれる)
var thisRenderer = this.GetComponent<Renderer>();
if(thisRenderer != null && thisRenderer.materials != null){
foreach(var m in thisRenderer.materials){
DestroyImmediate(m);
}
}
}
}
このコンポーネントを、PrefabのルートのGameObjectに貼っておけば、Prefab内のすべてのRendererおよびParticleSystemで、OnDestroy時にMaterialの解放がされるようになる。