Unity

【Unity】DestroyしたオブジェクトのMaterialがリークする問題への対応

More than 3 years have passed since last update.

Unity4.6での話です。
Unity5では未検証。

DestroyしたオブジェクトのMaterialがリークしていた

hogehoge.cs
// いろいろな処理

// プレハブから新たな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で計測してみた。

現状のメモリ使用量を計測

graph0710_1.png

開始時との差分(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で明示的に不使用アセットを解放してみることにした。

不使用アセットの強制解放

graph0710_2.png

開始時との差分(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

graph0710_2.png

開始時との差分(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

graph0710_4.png

開始時との差分(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に解放処理を書くのは面倒なので、自動解放用のスクリプトを書いた。

AutoDestroyMaterials.cs
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の解放がされるようになる。