この記事について
先日投稿した記事について再検証したところ、間違いや勘違いが多数見つかったため修正版を投稿します。
また、その際にいくらか細部について理解できた部分があるため、それらについて追記しています。
内容は大部分が元記事と重複しています。
元記事を読んでいない方が読みやすいよう、修正点についてはこの記事には記述しません。
修正箇所の確認は元記事をご参照ください。
UnityとIDisposable絡みのバグで一日悩んだのでメモ。
IDisposableというよりもファイナライザ(C#的にはデストラクタ)の問題なのだけど、 ファイナライザの主な用途がIDisposableだと思うのでこのタイトルで。
はじめに
IDisposable
はC#ではお馴染みのインターフェースで、
メモリなどのリソースを明示的に解放する必要がある場合(あるいは明示的に解放することに利点がある場合)に実装します。
using
ステートメントなどC#の言語仕様にまで組み込まれる重要なインターフェースです。
UnityにおいてはUnity自体がだいたいのリソース管理をやってくれるうえ、
MonoBehaviour
がOnDisable
やOnDestroy
といった機構を持っているためあまり出番はないのですが、
まったく使い道がないわけでもありません。
例えば、HideFlags.DontUnloadUnusedAsset
を適用したオブジェクトの管理が考えられます
(蛇足ですが、HideFlags.DontSave
やHideFlags.HideAndDontSave
を適用した場合も同様です)。
エディタ拡張を書くときなど、
Unityに勝手にリソースを回収してほしくない場合には、
対象のリソース(UnityEngine.Object
を継承した型)のhideFlags
にHideFlags.DontUnloadUnusedAsset
を設定します。
このフラグを設定すると、リソースがUnityの判断で解放されない代わりに、
DestroyImmediate
をリソースに対して呼び出して明示的に解放しなければならない義務が発生します。
これは、IDisposable
を利用すべきシチュエーションに合致しています。
というわけで、このような想定のもとIDisposable
を実装したクラスを書いていきます。
IDisposable 実装クラス
IDisposable
実装のテンプレートに従って、実装クラスを書くと次のようになります(人によって多少の違いはありますが)。
using System; using UnityEngine; public class DisposableObject : IDisposable { private ScriptableObject @object; private bool disposed; public DisposableObject() { @object = ScriptableObject.CreateInstance<ScriptableObject>(); @object.hideFlags = HideFlags.DontUnloadUnusedAsset; } ~DisposableObject() { if (!disposed) { Dispose(false); disposed = true; } } public void Dispose() { if (!disposed) { Dispose(true); GC.SuppressFinalize(this); disposed = true; } } protected virtual void Dispose(bool disposing) { UnityEngine.Object.DestroyImmediate(@object); } }
コンストラクタでHideFlags.DontUnloadUnusedAsset
を設定したScriptableObject
を生成して、
protected
なDispose
メソッドでDestroyImmediate
により解放。
IDisposable
で定義されているDispose
メソッドをpublic
で宣言し、
protected
なDispose
メソッドに引数true
を与えて呼び出すよう実装。
明示的に呼び出されなかった場合にも必ずprotected
なDispose
メソッドが呼び出されるように、
ファイナライザをprotected
なDispose
メソッドに引数false
を与えて呼び出すように実装。
まあ、いつものやつです。
protected
なDispose
メソッドの引数disposing
は明示的に呼び出されたか否かの確認に使います。
絶対に解放しなければならないリソースは引数の値に関わらず解放し、
解放するとパフォーマンスがよくなるよ程度のものであれば引数がtrue
のときだけ解放します。
このコードでは引数の値に関わらず常に解放するように記述しています。
IDisposable 実装クラスの使用
では、実際にこのクラスをスクリプトで使用してみます。
using UnityEngine; public class GameScript : MonoBehaviour { private DisposableObject disposable; private void Awake() { this.disposable = new DisposableObject(); } private void OnDestory() // typo { this.disposable.Dispose(); } }
シンプルなAwake
でオブジェクトを初期化し、OnDestroy
でオブジェクトを解放しようとするスクリプトです。
ただし、OnDestroy
の定義でタイプミスをしており、
実際にはOnDestory
メソッドは呼び出されず、Dispose
メソッドは呼ばれません。
このスクリプトを適当なゲームオブジェクトにアタッチして実行してみます。
すると、ゲームの終了時*1にUnityが次のようなエラーをはきます。
DestroyImmediate can only be called from the main thread.
何が起きたか
まあエラーの内容のままなのですが・・・
Unityは基本的にシングルスレッド設計です *2。 そのため、メインスレッド以外からUnityのAPIを呼び出そうとするとこのように拒否されます。
では、いったいどうして別スレッドから呼び出されたかというと、
protected
なDispose
メソッドがファイナライザを通して実行されたためです。
ファイナライザはファイナライズ用のスレッド上で実行されます。
そのため、DestroyImmediate
の呼び出しがファイナライズ用のスレッドから行われ、Unityに怒られるのです。
バグの連鎖
とはいえ、ここまでの話は特に大したことはありません。
IDisposable
の実装の仕方を知っているくらい知識があるならば、
先ほどのエラーメッセージで原因の推測はできます。
問題なのはここから・・・バグは連鎖するのです。
エラーメッセージの消失
IDisposable
実装クラスの解放部分のコードを次のように変更します。
protected virtual void Dispose(bool disposing) { if (@object != null) { UnityEngine.Object.DestroyImmediate(@object); @object = null; } }
すると、不思議な事にエラーメッセージが表示されなくなります。
これはゲーム終了時にUnityが自動的にScriptableObject
を回収することにより、条件式が偽と判定されたためです
(なぜ回収されてしまうかは後述)。
すでに回収されてしまったのだから改めて解放する必要もないし、UnityのAPIも呼び出していない、
ならばこれは正しい記述ではないかと思うかもしれません。
本当にそうでしょうか?
UnityはUnityEngine.Object
同士の比較に対してオーバーロードを定義しています。
よって、最初の条件式はUnityのAPIである静的なメソッドの呼び出しに置換されます。
つまり、やはりこの記述もメインスレッド以外からUnityのAPIを呼び出しています
*3。
閉じないエディタ
さて、はじめに自前にリソース管理をするべき状況の例としてエディタ拡張を挙げました。
というわけで、実装クラスの使用箇所をEditorWindow
内に変えてみましょう。
using UnityEditor; public class EditorScript : EditorWindow { private FatalDisposableObject disposable; [MenuItem("IDisposable/Open")] public static void Open() { EditorScript @this = GetWindow<EditorScript>(); @this.disposable = new FatalDisposableObject(); } }
これでIDisposable
実装クラスはうまくいけば(?!)Unityエディタの終了時まで生き残る(GCに回収されない)はずです。
ついでに、管理するリソースをもっと深刻なものに変えてみましょう。
using System; using UnityEngine; public class FatalDisposableObject : IDisposable { private RenderTexture @object; private bool disposed; public FatalDisposableObject() { @object = new RenderTexture(1024, 1024, 0, RenderTextureFormat.ARGB32); @object.hideFlags = HideFlags.DontUnloadUnusedAsset; @object.Create(); } ~FatalDisposableObject() { if (!disposed) { Dispose(false); disposed = true; } } public void Dispose() { if (!disposed) { Dispose(true); GC.SuppressFinalize(this); disposed = true; } } protected virtual void Dispose(bool disposing) { if (@object != null) { @object.Release(); UnityEngine.Object.DestroyImmediate(@object); @object = null; } } }
リソースをScriptableObject
からRenderTexture
に変えました。
RenderTexture
はUnityの外部にも依存する繊細なリソースです。
Unityも扱いには慎重なはず。
あとはつくったエディタウィンドウを開いてから、Unityエディタを閉じようとするだけで、 Unityが無限ループに嵌って動かなくなります *4。
これはファイナライザ実行のタイミングでUnityの管理するMonoオブジェクトがすでに破棄されているために起こります。 実際、停止時のログを見てみると、
GetManagerFromContext: pointer to object of manager 'MonoManager' is NULL (table index 5)
(Filename: C:/buildslave/unity/build/Runtime/BaseClasses/ManagerContext.cpp Line: 92)
となっており、すでにNULLとなっているMonoManager
へのアクセスが問題と推測されます。
無限ループに嵌る原因までは不明ですが、
演算子のオーバーロードによりリソースの存在を確認しようとしたときに、
その存在を管理していたオブジェクトが破棄されていたために何か問題が発生したのでしょう。
対処法
対処法としては簡単で、ファイナライザからUnityのAPIを呼び出さないようにするだけです。
具体的には、
disposing
引数によって場合分けする- そもそもファイナライザを定義しない
という二通りがあります。
基本的にはUnity関連のリソース管理にファイナライザはいらないと思うのですが、
IDisposable
のテンプレート実装に慣れていると書かないのが気持ち悪いので自分はdisposing
で場合分けしています。
ちなみに要注意なケース(というか今回自分がやらかしたこと)として、
IDisposable
なオブジェクトをラップしたIDisposable
なクラスを定義する場合があります。
下記はアウトです(理由は言うまでもないと思います)。
protected virtual void Dispose(bool disposing) { member.Dispose(); }
補足:リソースの行方
上述の対処法では明示的な解放をしなかった場合に、
結局リソースに対してDestroyImmediate
を呼び出していません。
そのとき解放されなかったリソースはどうなってしまうのか、という話。
Unity標準のプロファイラでメモリの項目を確認してみると、
HideFlags.DontUnloadUnusedAsset
を設定したリソースは"Scene Memory"から"Not Saved"に移されるものの、
存在自体はUnityも認識したままのようです。
ですので、絶対にすべて解放しなければならないタイミング(つまりはプログラムの終了時)ではすべて解放されるものと思われます。
補足:HideFlagsについて
「エラーメッセージの消失」の項でゲーム終了時にScriptableObject
が回収されると書きました。
HideFlags.DontUnloadUnusedAsset
を設定したにも関わらず何故でしょうか?
これはUnityがゲームの開始時と終了時にUnloadUnusedAssets
とは少し異なる方法でリソースの回収をするためのようです。
Unityはゲームの終了時にアセットやエディタで用いているもの以外のすべてのオブジェクトを破棄してきれいにした上で、
ゲーム開始前にシリアライズしておいたシーンをデシリアライズしようとします。
このとき、エディタで用いていないということを確認するためにHideFlags.DontSaveInEditor
の値を参照するようなのです。
HideFlags.DontUnloadUnusedAsset
の意味はUnloadUnusedAssets
で回収されないというだけで、
エディタ内のオブジェクトかゲーム内のオブジェクトかということは規定していないためこの用途では使えません。
一方、HideFlags.DontSaveInEditor
はスクリプトリファレンスによると、
The object will not be saved to the scene in the editor.
らしいので、こちらも本来の用途とは少し意味合いがずれていると思うのですが、 エディタ用かゲーム用かを判断するのにはこちらの方が適しているということなのでしょう。
まあ、実際にエディタ拡張でHideFlags
を使用する場合には、
HideFlags.DontSave
やHideFlags.HideAndDontSave
を使うことになるので、
これが問題になることはあまりないとは思います。
おわりに
今回、記事では順を追って説明しましたが、実際には突然エディタがフリーズする現象が発生したので(しかも何故か再現性が微妙だった)、 バグの特定に非常に苦労しました。
解放忘れなんて滅多に起きないと思うかもしれませんが、
どうも調べている限りEditorWindow
では普通に発生してしまう事態のようなのです。
その話はまたそのうち(次回?)。
執筆時のUnityのバージョン:5.4.0f3