この記事には修正版があります。
この記事は修正前にこの記事を読んだ方が修正箇所を確認するためにあります。
初めてこの記事を読む方は修正版をお読みください。
UnityとIDisposable絡みのバグで一日悩んだのでメモ。
IDisposableというよりもファイナライザ(C#的にはデストラクタ)の問題なのだけど、 ファイナライザの主な用途がIDisposableだと思うのでこのタイトルで。
【2016/09/02 追記】
【2016/09/02 修正】
はじめに
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; } }
すると、不思議な事にエラーメッセージが表示されなくなります。
デバッガをアタッチして詳細を追ってみると、どうも最初のnullチェックの部分で死んでいるようです。
UnityEngine.Object
に対する比較はオーバーロードが定義されているので、その内部で何かが起きているのだと推測されます。
エラーメッセージが表示されるということは、発生した問題がある意味では適切に処理されているということです。 つまり裏を返せば、表示されないということは・・・
【修正】
表示されない理由はUnityがゲーム終了時にScriptableObject
が回収してしまうためのようです。
結果として条件式が偽となるためDestroyImmediate
は呼び出されません。
しかし、条件式内でUnityのAPIである演算子のオーバーロードが呼び出されていることには注意すべきです。
デバッガをアタッチしたせいで、余計なプロパティやメソッドを呼び出してしまい、その結果のエラーをみて勘違いした模様。
【ここまで】
閉じないエディタ
さて、はじめに自前にリソース管理をするべき状況の例としてエディタ拡張を挙げました。
というわけで、実装クラスの使用箇所を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
に変えました。
これで解放されなかったときのリスクは重大です。
Unityはなんとしても解放したいはず。
あとはつくったエディタウィンドウを開いてから、Unityエディタを閉じようとするだけで、 Unityが無限ループに嵌って動かなくなります。
【追記】
どうもふつうに閉じられるときと閉じられないときとあるようです。
差がどこにあるのかは不明ですが、何度もウィンドウを開閉してから閉じようとすると固まる可能性が高いようです。
【修正】
特に開いた回数などは関係なかった模様。
原因から察するにGCがオブジェクトを処理する順番やスレッドの順番の問題かと。
修正版の記事には動作が停止した理由について、もう少し詳しく書きました。
【ここまで】
対処法
対処法としては簡単で、ファイナライザからUnityのAPIを呼び出さないようにするだけです。
具体的には、
disposing
引数によって場合分けする- そもそもファイナライザを定義しない
という二通りがあります。
基本的にはUnity関連のリソース管理にファイナライザはいらないと思うのですが、
IDisposable
のテンプレート実装に慣れていると書かないのが気持ち悪いので自分はdisposing
で場合分けしています。
ちなみに要注意なケース(というか今回自分がやらかしたこと)として、
IDisposable
なオブジェクトをラップしたIDisposable
なクラスを定義する場合があります。
下記はアウトです(理由は言うまでもないと思います)。
protected virtual void Dispose(bool disposing) { member.Dispose(); }
補足:リソースの行方 【追記】
上述の対処法では明示的な解放をしなかった場合に、
結局リソースに対してDestroyImmediate
を呼び出していません。
そのとき解放されなかったリソースはどうなってしまうのか、という話。
正直な話、私はあまり下層レイヤーに明るくなく、Unityの内部を知っているわけでもないのでよくわかりません。
しかし、HideFlags.DontUnloadUnusedAsset
の設定により、Unityがリソース管理を全面的にユーザコードに委ねているとは思えません。
おそらく、内部的にはどのようなリソースを使用中であるかという情報は管理していて、
絶対にすべて解放しなければならないタイミング(つまりはプログラムの終了時)ではすべて解放されるものと思われます。
あるいはOSレベルでそうなっているのかもしれません。
というわけで、私個人の見解としては「問題ないんじゃない?」という感じです。
もし詳しい方がいたら教えてください。
【修正】
Unity標準のプロファイラで確認したところ、やはりUnityが内部的に使用中か否か管理しているようでした。
【ここまで】
補足:HideFlagsについて 【追記】
はじめにエディタ拡張でリソースに対してHideFlags.DontUnloadUnusedAsset
を設定すると勝手に回収されなくなると書きました。
しかし、いくらか試してみたところ、どうもHideFlags.DontSaveInEditor
を設定していないとゲーム終了時に回収されてしまうようです。
HideFlags.DontSaveInEditor
はスクリプトリファレンスによると、
The object will not be saved to the scene in the editor.
らしいので、これが回収のフラグに使用されているのはおかしな感じがしますが、 ゲーム終了時にシーンのデシリアライズをしているのでそのあたりが何か関係しているのかもしれません(詳細不明)。
【修正】
HideFlags.DontSaveInEditor
の値によってリソースが回収される理由についてなんとなく推測して修正版に書きました。
【ここまで】
まあ、実際にエディタ拡張でHideFlags
を使用する場合には、
HideFlags.DontSave
やHideFlags.HideAndDontSave
を使うことになるので、
これが問題になることはあまりないとは思います。
おわりに
今回、記事では順を追って説明しましたが、実際には突然エディタがフリーズする現象が発生したので(しかも何故か再現性が微妙だった)、 バグの特定に非常に苦労しました。
また、解放忘れなんて滅多に起きないと思うかもしれませんが、
どうも調べている限りEditorWindow
では普通に発生してしまう事態のようなのです。
その話はまたそのうち(次回?)。
執筆時のUnityのバージョン:5.4.0f3
追記時のUnityのバージョン:同上