前回の記事や次回の記事を書くにあたってUnityのリソース管理についていろいろ調べたのでメモ。
前回の記事を書いたときも思ったのだけど、「リソース」ってどの範囲を指す言葉だろう。
「オブジェクト」とか「メモリ」とか他の言葉を使うことも考えたのですが、どれが正解かわからず、とりあえず「リソース」を使ってます。
まあ表現はともかく今回は、Unity内部でのUnityEngine.Object
の管理の話です。
表現がおかしかったら適宜読み替えてください。
はじめに
Unityのエディタ拡張を書いていると、変数の値が突然nullになるなど、おかしなバグに悩まされることがあります。
HideFlags
などこれらの動作を制御できるものが存在することは以前から知っていましたが、
スクリプトリファレンスを読んでもいまいち理解できなかったので、
実際にいろいろと設定しながら調べてみました。
あらかじめ断っておくと、 この記事にはUnityのプロファイラなどを使って実際に確認した動作から推測した内容が含まれています。 以降の情報が正確とは限らないことに留意してお読みください。
Unityのリソース管理区分
Unity標準のプロファイラを開き、メモリの項目でスナップショットをとると次の五つの項目に分類されて表示されます (説明はマニュアルより)。
分類 | 説明 |
---|---|
Scene Memory | GameObject and attached components |
Not Saved | GameObjects marked as DontSave |
Assets | Asset referenced from user or native code |
Builtin Resources | Unity Editor resources or Unity default resources |
Other | GameObjects not marked in the above categories |
正直、マニュアルの説明があまり正確とは思えないのですが、大体の雰囲気はわかると思います。 おそらくUnityはこのような区分のもとリソースを管理していると思われます。
"Builtin Resources"と"Other"については基本的にUnityエディタやUnityエンジンの利用しているメモリのようですので、 今回はこの部分には触れません。
というわけで、残りの三つについてリソースがどのように区分されるのかを見ていきます。
リソースの生成
まずはScriptableObject
を継承したクラスFoo
を生成してどこに記録されるかを見てみます。
var foo = CreateInstance<Foo>();
"Scene Memory" > "MonoBehaviour" とみていくと"Foo"の項目が追加されているのがわかります。 つまり、普通にインスタンスを生成した場合にはすべて"Scene Memory"に記録されるようです。
今回、EditorWindow
継承クラスのOnGUI
からScriptableObject
継承クラスを生成したにも関わらず、
"Scene Memory"に記録されたということはあまり"Scene"という言葉に惑わされないほうがよさそうです。
もちろんシーンのヒエラルキーの情報も記録されていますが、それ以外もまずはここに記録されます
(むしろ シーン=ヒエラルキー という発想を捨てた方がいいのかもしれません、LightmapSettings
なんかもシーンの一部なので)。
DontUnloadUnusedAssetの設定
次にFoo
に対してHideFlags.DontUnloadUnusedAsset
を設定してみます。
var foo = CreateInstance<Foo>(); foo.hideFlags |= HideFlags.DontUnloadUnusedAsset;
今度は"Not Saved" > "MonoBehaviour" に"Foo"の項目が追加されているのがわかります。
つまり、HideFlags.DontUnloadUnusedAsset
を設定したリソースは"Not Saved"に記録されます。
ちなみに、HideFlags.DontUnloadUnusedAsset
をクリアすると"Scene Memory"に戻ります。
余談ですが、EditorWindow
なんかはHideFlags.DontSave
の設定されたScriptableObject
の継承クラスなので、
カスタムエディタウィンドウを作成すると"Not Saved"に値が記録されます
*1。
AssetDatabaseに登録
次はFoo
をアセットとして保存してみます。
アセットとして保存するためにはAssetDatabase
クラスのメソッドを使用します。
var foo = CreateInstance<Foo>();
AssetDatabase.CreateAsset(foo, "Assets/foo.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
今度は"Assets" > "MonoBehaviour" に"Foo"の項目が追加されます。 つまり、アセットとして保存したリソースは"Assets"に記録されます。
アセットを削除すると"Assets"から削除され、リソースは解放されます。 "Not Saved"とは異なり、"Scene Memory"には戻りません。
AssetDatabase.DeleteAsset("Assets/foo.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
両方やってみる
最後にHideFlags.DontUnloadUnusedAsset
の設定とAssetDatabase
への登録をどちらもやってみます。
var foo = CreateInstance<Foo>();
foo.hideFlags |= HideFlags.DontUnloadUnusedAsset;
AssetDatabase.CreateAsset(foo, "Assets/foo.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
結果は"Not Saved"でした。
ただし、HideFlags.DontUnloadUnusedAsset
をクリアすると"Assets"に移動します。
まとめ
HideFlags.DontUnloadUnusedAsset
が設定されているなら"Not Saved"- アセットとして登録されているなら"Assets"
- どちらでもないなら"Scene Memory"
リソースの回収
Unityエディタではいくつかのタイミングでリソースが自動回収されます。 これには大きく次の二種類があるようです。
- UnloadUnusedAssets による不要なリソースの解放
- ゲームの開始時および終了時のシーンのクリア
UnloadUnusedAssets による不要なリソースの解放
UnloadUnusedAssets
はGCのようにどこからも参照されていない不要なリソースを探して解放します。
その方法はルートとなるオブジェクト群から順に参照を辿って到達できるリソースをマークしていき、 すべてを終わったときにマークされていなかったリソースを不要と判断して解放するという方法です。 ここで、ルートとなるオブジェクト群とは何かというのが問題となってきます。
スクリプトリファレンスによると、 ヒエラルキー全体と静的変数と書いてあります。 しかし、これはゲーム内における場合です。 エディタ拡張も含めた場合には表示中のエディタもルートとなるようです。
また、当然ですが"Not Saved"に属するリソースはUnloadUnusedAssets
では解放されません。
ちなみにUnloadUnusedAssets
には次の三種類があります。
Resources.UnloadUnusedAssets()
EditorUtility.UnloadUnusedAssetsImmediate(true)
EditorUtility.UnloadUnusedAssetsImmediate(false)
ひとつめはゲーム用で、ゲーム内でのみ動作します。
あとの二つはエディタ用で引数がtrue
だとすべての参照を、引数がfalse
だとシリアライズ対象の参照のみを辿るようです。
ゲームの開始時および終了時のシーンのクリア
Unityはゲームの開始時と終了時にシーンをまっさらな状態にするため、
DestroyWorldObjects
という何やらやばそうな関数を呼んで、
アセットでもエディタ要素でもないリソースを解放しようとします。
ここで問題なのはエディタ要素かどうかをどう判定するかです。
残念ながら、HideFlags.DontUnloadUnusedAsset
はエディタとゲームを分けるようなフラグとはなっていません。
そこで、Unityは代わりにHideFlags.DontSaveInEditor
を使用するようです。
HideFlags.DontSaveInEditor
は対象オブジェクトをエディタ中のシーンに保存しないという意味なので、
これを設定していないからといって解放するのもどうかと思うのですが、
エディタ要素なら設定してしかるべきということなのかもしれません。
ちなみにHideFlags.DontSaveInEditor
は単独で設定しても、"Not Saved"ではなく"Scene Memory"に記録されます。
なんか名前おかしくない・・・?
まとめ
HideFlags.DontUnloadUnusedAsset
とHideFlags.DontSaveInEditor
を設定しておけば勝手に解放はされない。- 特に問題なければ、
HideFlags.DontSave
かHideFlags.HideAndDontSave
検証用ソースコード
using UnityEngine; public class Foo : ScriptableObject { }
using UnityEngine; using UnityEditor; public class ResourceManagement : EditorWindow { [SerializeField] private Foo foo; private Foo _foo; [MenuItem("ResourceManagement/Open")] private static void Open() { GetWindow<ResourceManagement>(); } private void OnGUI() { if (GUILayout.Button("Print")) { Debug.LogFormat("foo: {0}({1}), _foo: {2}({3})", foo ? foo.GetInstanceID().ToString() : "null", foo ? foo.hideFlags.ToString() : "null", _foo ? _foo.GetInstanceID().ToString() : "null", _foo ? _foo.hideFlags.ToString() : "null"); } if (GUILayout.Button("New")) { foo = CreateInstance<Foo>(); foo.name = "foo"; _foo = CreateInstance<Foo>(); _foo.name = "_foo"; } if (GUILayout.Button("Delete")) { DestroyImmediate(foo, true); DestroyImmediate(_foo, true); } if (GUILayout.Button("Set Null")) { foo = null; _foo = null; } if (GUILayout.Button("Clear HideFlags")) { foo.hideFlags = 0; _foo.hideFlags = 0; } if (GUILayout.Button("Set DontSaveInEditor")) { foo.hideFlags |= HideFlags.DontSaveInEditor; _foo.hideFlags |= HideFlags.DontSaveInEditor; } if (GUILayout.Button("Set DontUnloadUnusedAsset")) { foo.hideFlags |= HideFlags.DontUnloadUnusedAsset; _foo.hideFlags |= HideFlags.DontUnloadUnusedAsset; } if (GUILayout.Button("Save As Asset")) { AssetDatabase.CreateAsset(foo, "Assets/foo.asset"); AssetDatabase.CreateAsset(_foo, "Assets/_foo.asset"); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } if (GUILayout.Button("Delete Asset")) { AssetDatabase.DeleteAsset("Assets/foo.asset"); AssetDatabase.DeleteAsset("Assets/_foo.asset"); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } if (GUILayout.Button("UnloadUnusedAssets")) { Resources.UnloadUnusedAssets(); } if (GUILayout.Button("UnloadUnusedAssetsImmediate(false)")) { EditorUtility.UnloadUnusedAssetsImmediate(false); } if (GUILayout.Button("UnloadUnusedAssetsImmediate(true)")) { EditorUtility.UnloadUnusedAssetsImmediate(true); } } }
おわりに
Unityのエディタ拡張がうまく書けなくてつらい今日この頃。 おかげで、Unityのデバッグ技術だけは無駄に向上してます (プロファイラ眺めたり、エディタログ読んだり、フルのコールスタック表示したり、デバッガでメモリ書き換えたり・・・)。
最近の記事は内容的にちょっと硬いので、なんとなくキャッチーなタイトルつけて興味を引いてみたりしていたのですが、 ブクマとかつかないあたりみるとやっぱり需要ないんですかね・・・
執筆時のUnityのバージョン:5.4.0f3
*1:DontSave は DontSaveInBuild と DontSaveInEditor と DontUnloadUnusedAsset の複合フラグ