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

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](https://qiita-image-store.s3.amazonaws.com/0/78568/490d7dd3-14f4-56d1-cbb5-231fe78d4fb4.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](https://qiita-image-store.s3.amazonaws.com/0/78568/26c5c2f6-9689-16fa-7cf9-c646fe48f237.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](https://qiita-image-store.s3.amazonaws.com/0/78568/5d52bb1e-549e-03bb-3371-e0a7e9e0cc4d.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](https://qiita-image-store.s3.amazonaws.com/0/78568/4070bff0-cf50-8f1d-b454-08f0d80386b1.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の解放がされるようになる。