【Unity】【C#】インターフェイスの有無を調べて、自動 Dispose する Destroy を作る 
2021/03/03 Wed [edit]
今回は動的に生成したオブジェクト等を確実に破棄するための自動 Dispose を走らせるような Destroy を作ってみよう。実際、Unity では GameObject を Destroy しても、プロファイラで見てみると、それに使っているテクスチャやマテリアル等はメモリに残っているようだ。その場合、Resources.UnloadUnusedAssets を使えば、メモリから消えてはくれるが、このメソッドは GC 回収等をするので、一瞬画面が停止(プチフリーズ)することが多い。なので、あまり頻繁には使えないのが難点だ(画面遷移や Loading中 等、あまり目立たないタイミングで使うと良い)。
大雑把な実装としては「インターフェイスの有無を調べる」ことと、それを「拡張メソッド Destroy で走らせる」こととなる。それではやってみよう。
(※) Unity 2019.4.17f1 / Windows10(x64) で確認
■インターフェイスの有無を調べる(IDisposable の取得)
まずは、インターフェイスの有無だが、これは調べたいクラスやインターフェイスなどをフルパスで与えれば、以下のようなメソッドで簡単に取得できる。
●インターフェイスの有無を調べる
using System;
var inf = obj.GetType().GetInterface("System.IDisposable"); //ここでは IDisposable インターフェイスを指定
ここでは "System.IDisposable" を指定しているが、同じように他のクラスやインターフェイスでも文字列で指定すれば取得できる。取得できなかった場合は null が返ってくるので、例えば「このオブジェクトには IDisposable が実装されているか?」を調べるメソッドは以下のように書ける。
●IDisposable を実装しているかを調べる拡張メソッド
using System;
public static class Extensions //名前は任意
{
/// <summary>
/// System.IDisposable を実装しているか?
/// 2021/03/03 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-393.html
/// </summary>
/// <param name="obj">対象オブジェクト</param>
/// <returns>true = 実装している</returns>
public static bool HasDispose(this object obj)
{
if (obj != null)
{
return obj.GetType().GetInterface("System.IDisposable") != null;
}
return false;
}
}
.NET 4.x 以降なら、以下のように簡潔に書いても良いかもね。
public static bool HasDispose(this object obj)
=> (obj != null) ? (obj.GetType().GetInterface("System.IDisposable") != null) : false;
まぁ、書き方は好みで(笑)。1文にまとめると短くて済むが、後から条件を加えたいときなどは、かえってごちゃごちゃと読みにくくなる。この辺りはケースバイケースだね(このメソッド場合、あまり書き換えることも無いので、良いと思うが)。
■自動 Dispose する拡張 Destroy を作る
せっかくなので、先に作った HasDispose を使って、拡張メソッド Destroy で自動的に IDisposable を走らせるようにしてみよう。なんのことは無い、よくある null チェック付きの拡張メソッドに HasDispose を入れただけのものである。でもこれだけで、Dispose() を走らせるのを忘れるのを防げるので、結構有用だったりする。
●実装してるなら Dispose してから、Destroy する (コンポーネント単体用)
using System;
using UnityEngine;
public static class Extensions //名前は任意
{
/// <summary>
/// null でないとき、実装してるなら Dispose してから、Destroy する。
/// ※どちらかというと、コンポーネント単体用。
/// 2021/03/03 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-393.html
/// </summary>
/// <param name="obj">対象オブジェクト</param>
public static void SafeDestroy(this UnityEngine.Object obj)
{
if (obj != null)
{
if (obj.HasDispose())
{
((IDisposable)obj).Dispose();
}
UnityEngine.Object.Destroy(obj);
}
}
}
使い方は簡単で、例えば Instantiate したオブジェクトなどがあったら、そのオブジェクト(obj とする)を obj.SafeDestroy() とするだけだ。
ただ、気をつけなくてはいけないのは、SafeDestroy を使うときの引数の「型」には注意。
というのは、継承を多用しているときに、サブクラスに Dispose が実装してあっても、ベースクラスの方を引数に渡してしまうと、ベースクラスには Dispose が実装されてないので、実行されない。これはこのメソッドに限らずよくやってしまうミスだ。特に Unity では GetComponent で特定の型を取得して、処理させたりすることも多いが、その型に Dispose が無ければ、当然実行されない(Destroy はされる)。
その場合は少し面倒だが、ベースクラスを override できるように予め Dispose を実装しておき、サブクラスからも呼んでおくことで解決できる。例えば、以下のように書いておけば良い。
●サブクラスの Dispose
public new void Dispose() //ベースクラスが virtual なら、サブクラスは override でも良い
{
base.Dispose(); //ベースクラスの Dispose を実行
}
ここではオーバーライドに new を使っているが、ベースクラスが virtual で実装してあるなら、override が良いだろう。
また、GameObject を型とするなら、GetComponents が使えるので、単純にループを回した方が簡単だろう。
●GameObject の全てのコンポーネントを Dispose してから Destroy する (GameObject用)
using System;
using UnityEngine;
public static class Extensions //名前は任意
{
/// <summary>
/// GameObject の全てのコンポーネントを Dispose してから Destroy する
/// 2021/03/03 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-393.html
/// </summary>
/// <param name="obj">対象オブジェクト</param>
public static void SafeDestroy(this GameObject obj)
{
if (obj != null)
{
var infs = obj.GetComponents<IDisposable>();
if (infs != null)
{
foreach (var item in infs)
item.Dispose();
}
GameObject.Destroy(obj);
}
}
}
まぁ、MonoBehaviour を使ってるオブジェクトの場合は OnDestroy が呼ばれるので、そこから Dispose を呼ぶだけでも良いと思う。
私の場合、例えば VRM Live Viewer でサムネイルを大量に動的生成しているが、初期の頃は Destroy しても、テクスチャがメモリには残っているようで(プロファイラで見るとわかる)、スマホではよく落ちていた。なので、今はそれを確実に消すために、サムネイルのコンポーネントにテクスチャを破棄する Dispose を入れるようにしている。そうしないと、特にスマホアプリでは自動に任せてると、メモリ不足に陥るんだよね(Resources.UnloadUnusedAssets も使ってるが、冒頭に書いたようにプチフリーズが頻繁に起こるので、タイミングを見計らって使うしかない)。
まぁ「なるべく明示的に破棄処理を実行する」ようにするだけのものなので、PC 等メモリに余裕がある機器なら、ある程度システムに任せても良いだろう。しかし、Unity って意外と素材関連(テクスチャやマテリアル、シェーダ等)はメモリに残り続けるので、長い時間アプリを使ってると、いつまでも解放されずにメモリリークしやすい。ランタイム中、少しでもメモリ確保したいときには、こういったメソッドを利用して、使い終わったオブジェクトを確実に破棄していくのもアリだろう。
(関連記事)
【Unity】【C#】動的にオーディオファイルの読み込みと再生をする
【Unity】【C#】Windows で mp3 をランタイムで再生する
【Unity】【C#】TGAをランタイムでロードする
【Unity】【C#】BMP をランタイムで読み込む
【Unity】【C#】Android で VRM(VRoid)を動的に読み込む
- 関連記事
トラックバック
トラックバックURL
→http://fantom1x.blog130.fc2.com/tb.php/393-8bd66be8
この記事にトラックバックする(FC2ブログユーザー)
| h o m e |