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
を代入するとアンロードされるようになる