Unity Advent Calendar 21日目の記事です。
書くネタがなかったので、地味ですがResources.UnloadUnusedAssetsに関する話をひとつ書きます。
GameObjectをDestroyしてResources.UnloadUnusedAssetsを実行しても、そのGameObjectで使われていたアセットが解放されない場合があるという話です。
なお検証はUnity 5.3.0f4で行っています。
Resources.UnloadUnusedAssets
一応Resources.UnloadUnusedAssetsについて簡単に説明します。リファレンスはこちら。
次のプログラムではResources.Loadでテクスチャをロードしたあと、その後それが不要になった後でResources.UnloadUnusedAssetsを呼び出しています。
using UnityEngine;
using System;
using System.Collections;
public class Test1 : MonoBehaviour
{
bool doNext;
IEnumerator Start ()
{
// テクスチャをロード
var texture = Resources.Load<Texture> ("sample");
Debug.Log (texture.name);
// (1)
yield return StartCoroutine (Wait ());
/* ...textureを使ったなんらかの処理... */
// テクスチャへの参照を捨ててGC実行
texture = null;
GC.Collect ();
// (2)
yield return StartCoroutine (Wait ());
// Resources.UnloadUnusedAssetsを実行
yield return Resources.UnloadUnusedAssets ();
// (3)
yield return StartCoroutine (Wait ());
}
void OnGUI ()
{
if (GUILayout.Button ("Do Next")) {
doNext = true;
}
}
IEnumerator Wait ()
{
while (!doNext) {
yield return null;
}
doNext = false;
}
}
(1)(2)(3)のそれぞれの時点でのテクスチャロード状況をUnity Profilerから確認してみます。
(1)の時点でテクスチャのRef Countが1になっていたのが(2)の時点では0になってますが、それでもメモリ上にはテクスチャが残っています。Resources.UnloadUnusedAssets呼び出し後の(3)の時点でテクスチャがリストから消え、メモリ上からアンロードされたことがわかります。
なおEditor上ではResources.UnloadUnusedAssetsを実行してもアンロードされない場合があります。検証の際はビルドして行ってください。
DestroyしてUnloadUnusedAssetsしても解放されないケース
ここから本題です。
先のコードを一部変更し、テクスチャをResources.Loadでロードするのではなく、テクスチャを持ったプレハブからゲームオブジェクトを生成するようにしました。なおStart以外のメソッドについては先のサンプルと同じです。
IEnumerator Start ()
{
// テクスチャへの参照を持ったassetContainerを生成
// assetContainer.textureフィールドにはインスペクタから適当なテクスチャが設定されている
var assetContainer = Instantiate (Resources.Load<AssetContainer> ("samplePrefab"));
Debug.Log (assetContainer.texture.name);
// (1)
yield return StartCoroutine (Wait ());
// assetContainerを破棄
Destroy (assetContainer.gameObject);
yield return null;
// (2)
yield return StartCoroutine (Wait ());
// Resources.UnloadUnusedAssetsを実行
yield return Resources.UnloadUnusedAssets ();
// (3)
yield return StartCoroutine (Wait ());
}
public class AssetContainer : MonoBehaviour
{
public Texture texture;
}
assetContainerがtextureフィールドを持っていて、そこからテクスチャが参照されています。Destroy(assetContainer)してからResources.UnloadUnusedAssetsを実行することでそのテクスチャがアンロードされることを意図した形になってます。
このプログラムを実行し、(3)の時点でのアセットのロード状況を確認します。
Resources.UnloadUnusedAssets呼び出した後の(3)の時点でも、テクスチャがアンロードされずに残ってしまっています。そのためこのままStartメソッド内で処理を続けるとメモリリークということになります。これが今回説明する問題のケースです。
原因
原因について説明します。
Resources.UnloadUnusedAssetsの実装を見ることはできないので推測ですが、Destroy(assetContainer.gameObject)を実行してもMonoレイヤー上ではassetContainer変数がnullになっていないことが原因と思われます。
説明のために先のプログラムの(3)の後に次のコードを追加して実行してみます。
Debug.Log (assetContainer == null); //=> true
Debug.Log (assetContainer.texture.name); //=> sample
assetContainer == nullはtrueを返しているので、その後のassetContainer.texture.nameへのアクセスは通常であればNullReferenceExceptionが発生するのですが、実行してみると発生しません。
これはMonoBehaviourやTextureのベースオブジェクトであるUnityEngine.Objectが、==演算子のオーバーロードをしており、Monoレイヤー上ではnullでないものが==を使うとnullとされてしまうことが原因です。
これについて詳しくは下記のページなどで説明されてます。(一番上は公式のブログポストです)
http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
http://qiita.com/satanabe1@github/items/e896303859be5d42c188
http://tsubakit1.hateblo.jp/entry/2015/05/28/014440
推測ですが、Resources.UnloadUnusedAssetsはMonoレイヤー上で参照されなくなったアセットをアンロードするような実装になっていて、そのためDestroyしたがMonoレイヤー上で参照が残っている場合に、そのアセットがアンロードされずに残ってしまうのではないかと思われます。
対処
Monoレイヤー上でもゲームオブジェクトを参照する変数の値をnullにしてやり、テクスチャへの参照が残っていない状態にすれば解決します。
先のプログラムのDestroy(assetContainer.gameObject)の箇所を次のように書き換えて実行してみます。
// assetContainerを破棄し、さらにnull代入
Destroy (assetContainer.gameObject);
assetContainer = null; // <- この行を追加
yield return null;
これを実行した結果が下の画像です。
テクスチャがアンロードされていることがわかります。
また他に、AssetContainerのOnDestroyでtextureフィールドにnullを代入するなどの対処法も考えられます。
なお、今回はコルーチンの中でResources.UnloadUnusedAssetsを呼び出しましたが、コルーチンが終了すればassetContainerを参照しているところはなくなるのでその後Resources.UnloadUnsedAssetsを呼び出せば問題なくアンロードされます。要はどこからも(本当に)参照されてなければそれでよいということです。
まとめ
-
GameObjectをDestroyした後でResources.UnloadUnusedAssetsを実行してもアセットがアンロードされない場合がある - その
GameObjectを参照していた変数にnullを代入したり、OnDestroyでアセットを参照するフィールドにnullを代入するとアンロードされるようになる




