10
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

メモリリークを防ぐテンプレ&検出するエディター拡張

Last updated at Posted at 2023-09-21

更新履歴: 名前やタグに依存した Unity の一部機能で起きる LMS & エラーの回避策を追加。IDE エディター上でのエラーチェックをより厳密に。
 

Leaked Managed Shell って名前が付いたことで問題の認識がしやすくなったのは良いことですね。

👉 Leaked Managed Shell とは

全てのケースでヌル代入が必要なわけじゃないと思うけど、統一してしまった方が楽だろうってことでテンプレートなヘルパーを使う試み。

ManagedShell クラス

このヘルパーを使うと無駄に確保され続けるメモリの解放の他、エディター上でのテストを考慮して Destroy()DestroyImmediate() を使い分ける面倒と DestroyImmediate 直後の「削除されたオブジェクトにアクセスしようとしてるよ」エラーからも解放される。

オブジェクトを破棄したフレーム内限定で起きる問題を回避するための処理も行っている

ヘルパーには単体でオブジェクトを渡す以外に T[] List<T> Dictionary<TKey, TValue> ICollection<T> を渡せるオーバーロードがある。

// オブジェクト単体の破棄
ManagedShell.Dispose(ref unityObj);
// 書き込み可能なコレクションは ref で渡せばコレクション自体も破棄する(ヌル代入する
ManagedShell.Dispose(ref Array_or_Collection);
// readonly なコレクションは無印で渡せば全要素を破棄した後に Clear() する(コレクション自体は残る
ManagedShell.Dispose(ReadOnly_Array_or_Collection);

Dictionary<TKey, TValue> はどんな型でも突っ込めて IDisposable を実装している非 Unity オブジェクトだった場合は Dispose() する

プレハブのルートを GameObject じゃなくて Transform で握ってるような時に使いやすい DisposeGameObject メソッドも用意。

// Object.Destroy(component.gameObject); して component = null; するイメージ
void DisposeGameObject<T>(ref T component) where T : UnityEngine.Component
// 使い方は Dispose() と同じ
ManagedShell.DisposeGameObject(ref component);
ManagedShell.DisposeGameObject(ref componentArrayOrCollection);
ManagedShell.DisposeGameObject(readonlyComponentCollection);

Destroy(Transform) はエラーは出ずに何も起きないだけだったような? → 💬

テンプレートコード

プロジェクト間で移植した時の競合を回避するには~~とか、複数のアセンブリから使う場合の切り分けと参照は~~とか、やろうとしてる事の割に面倒が増えないようにテンプレートを使って各アセンブリ(.dll)それぞれがヘルパークラスを持つ、って運用にするのが楽。

Note
オブジェクトを破棄したフレーム内限定で起きる問題への対処を行う処理はプリプロセッサーシンボル MANAGED_SHELL_DISABLE_DEEP_DESTROY を設定すると無効になる。

Note
IDisposable を実装してるかのチェックもしてる(OnDestroy があるからあり得ないか?
プリプロセッサーシンボル MANAGED_SHELL_DISABLE_DISPOSABLE_CHECKS で無効になる。

ManagedShell テンプレート

テンプレートファイルを元に新規スクリプトを作るメソッドは

👉 UnityEditor.ProjectWindowUtil.CreateScriptAssetFromTemplateFile(...)

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

#ROOTNAMESPACEBEGIN#
    internal static class ManagedShell
    {
        const int LAYER_DEFAULT = 0;
        const string TAG_DEFAULT = "Untagged";
        const string NAME_PREFIX = "__MS_DESTROYED__";

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static void Dispose_Internal<T>(T obj, float t = 0f)
            where T : UnityEngine.Object
        {
            if (obj == null || obj is Transform)
            {
                //UnityEngine.Debug.LogWarning("[ManagedShell] destroying null or transform doesn't make sense: " + obj);
                return;
            }

#if false == MANAGED_SHELL_DISABLE_DEEP_DESTROY
            // NOTE: as of actual object deletion is happened at the end of frame, GameObject.Find(),
            //       GameObject.FindWithTag() or other Unity native functions unexpectedly retrieve
            //       destroyed objects. thus need to "hide" destroyed object from those functions.
            //       * error could occur if IDisposable.Dispose() is depending on modified properties.
            //         however it's edge case and here is only chance to hide object before destroy.
            if (t <= 0)
            {
                // component.name will change gameObject.name. ignore it.
                if (obj is not Component)
                    // 1) must be unique due to obj.name may be used as dictionary key
                    // 2) empty or short string causes problem when slicing or something w/o bounds check
                    obj.name = NAME_PREFIX + obj.GetHashCode();

                if (obj is GameObject go)
                {
                    go.SetActive(false);
                    // to hide from GetComponentsInChildren<T>(true)
                    go.transform.SetParent(null, false);
                    go.tag = TAG_DEFAULT;
                    go.layer = LAYER_DEFAULT;
                    //go.hideFlags = ;
                }
            }
#endif

#if false == MANAGED_SHELL_DISABLE_DISPOSABLE_CHECKS
            // mono behaviour might implement IDisposable. super ultra rare case.
            if (obj is IDisposable disposable)
            {
                disposable.Dispose();
                if (obj == null)
                    return;
            }
#endif

#if UNITY_EDITOR
            if (Application.IsPlaying(obj))
                UnityEngine.Object.Destroy(obj, t);
            else
                UnityEngine.Object.DestroyImmediate(obj);
#else
            UnityEngine.Object.Destroy(obj, t);
#endif
        }


        ///<summary>NOTE: Do nothing when obj is null or Transform component.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref T obj, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose_Internal(obj, t);
            obj = null;
        }

        ///<summary>
        /// This method ensures variable doesn't have reference to deleted component.
        /// Especially useful when destroying GameObject by Transform reference.
        ///</summary>
        ///<remarks>Shorthand for `Destroy(component.gameObject); component = null;`</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref T component, float t = 0f)
            where T : Component
        {
            Dispose_Internal(component.gameObject, t);
            component = null;
        }


        /*  Transform overloads  ================================================================ */

        // NOTE: these overloads are to show error when trying to dispose Transform instance
        const string ERROR_TRANSFORM = "Disposing Transform does nothing. Use DisposeGameObject() instead.";

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref Transform transform, float t = 0f)
        {
            // TODO: [Obsolete("message", true)] sometimes doesn't show error on Visual Studio.
            //       don't throw for workaround.
            //throw new NotSupportedException(ERROR_TRANSFORM);
        }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(Transform[] array, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref Transform[] array, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(List<Transform> list, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref List<Transform> list, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ICollection<Transform> collection, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref ICollection<Transform> collection, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(Dictionary<Transform, Transform> dict, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose(ref Dictionary<Transform, Transform> dict, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(Dictionary<T, Transform> dict, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(ref Dictionary<T, Transform> dict, float t = 0f) { }

        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(Dictionary<Transform, T> dict, float t = 0f) { }
        [Obsolete(ERROR_TRANSFORM, true)]
        internal static void Dispose<T>(ref Dictionary<Transform, T> dict, float t = 0f) { }


        /*  Dispose collection overloads  ================================================================ */

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(T[] array, float t = 0f)
            where T : UnityEngine.Object
        {
            if (array == null)
                return;

            for (int i = 0; i < array.Length; i++)
            {
                Dispose_Internal(array[i], t);
                array[i] = null;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref T[] array, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose(array, t);
            array = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(List<T> list, float t = 0f)
            where T : UnityEngine.Object
        {
            if (list == null)
                return;

            for (int i = 0; i < list.Count; i++)
            {
                Dispose_Internal(list[i], t);
                //list[i] = null;
            }
            list.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref List<T> list, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose(list, t);
            list = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ICollection<T> collection, float t = 0f)
            where T : UnityEngine.Object
        {
            if (collection == null)
                return;

            foreach (var item in collection)
            {
                Dispose_Internal(item, t);
            }
            collection.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T>(ref ICollection<T> collection, float t = 0f)
            where T : UnityEngine.Object
        {
            Dispose(collection, t);
            collection = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<TKey, TValue>(Dictionary<TKey, TValue> dict, float t = 0f)
        {
            if (dict == null)
                return;

            foreach (var key in dict.Keys)
            {
                //value
                if (dict[key] is UnityEngine.Object valOb)
                {
                    Dispose_Internal(valOb, t);
                    //dict[key] = default;
                }
                else if (dict[key] is IDisposable disposable)
                {
                    disposable.Dispose();
                }

                //key
                if (key is UnityEngine.Object keyOb)
                {
                    //dict.Remove(key);
                    Dispose_Internal(keyOb, t);
                }
                else if (key is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
            dict.Clear();
        }

        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<TKey, TValue>(ref Dictionary<TKey, TValue> dict, float t = 0f)
        {
            Dispose(dict, t);
            dict = null;
        }


        /*  DisposeGameObject collection overloads  ================================================================ */

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(T[] array, float t = 0f)
            where T : Component
        {
            if (array == null)
                return;

            for (int i = 0; i < array.Length; i++)
            {
                Dispose_Internal(array[i].gameObject, t);
                array[i] = null;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref T[] array, float t = 0f)
            where T : Component
        {
            DisposeGameObject(array, t);
            array = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(List<T> list, float t = 0f)
            where T : Component
        {
            if (list == null)
                return;

            for (int i = 0; i < list.Count; i++)
            {
                Dispose_Internal(list[i].gameObject, t);
                //list[i] = null;
            }
            list.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref List<T> list, float t = 0f)
            where T : Component
        {
            DisposeGameObject(list, t);
            list = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ICollection<T> collection, float t = 0f)
            where T : Component
        {
            if (collection == null)
                return;

            foreach (var item in collection)
            {
                Dispose_Internal(item.gameObject, t);
            }
            collection.Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<T>(ref ICollection<T> collection, float t = 0f)
            where T : Component
        {
            DisposeGameObject(collection, t);
            collection = null;
        }

        ///<summary>For readonly collection to dispose only elements.<br/>Use `ref` overload instead to dispose elements and collection together.</summary>
        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<TKey, TValue>(Dictionary<TKey, TValue> dict, float t = 0f)
        {
            if (dict == null)
                return;

            foreach (var key in dict.Keys)
            {
                //value
                if (dict[key] is Component valOb)
                {
                    Dispose_Internal(valOb.gameObject, t);
                    //dict[key] = default;
                }
                else if (dict[key] is IDisposable disposable)
                {
                    disposable.Dispose();
                }

                //key
                if (key is Component keyOb)
                {
                    //dict.Remove(key);
                    Dispose_Internal(keyOb.gameObject, t);
                }
                else if (key is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
            dict.Clear();
        }

        ///<remarks>Dispose() will be called if TKey and/or TValue implements IDisposable.</remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void DisposeGameObject<TKey, TValue>(ref Dictionary<TKey, TValue> dict, float t = 0f)
        {
            DisposeGameObject(dict, t);
            dict = null;
        }


        /*  overloads  ================================================================ */

#if false

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2>(
            ref T1 o1, ref T2 o2, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3>(
            ref T1 o1, ref T2 o2, ref T3 o3, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4, T5>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, ref T5 o5, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
            where T5 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
            Dispose(ref o5, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4, T5, T6>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, ref T5 o5, ref T6 o6, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
            where T5 : UnityEngine.Object
            where T6 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
            Dispose(ref o5, t);
            Dispose(ref o6, t);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void Dispose<T1, T2, T3, T4, T5, T6, T7>(
            ref T1 o1, ref T2 o2, ref T3 o3, ref T4 o4, ref T5 o5, ref T6 o6, ref T7 o7, float t = 0f)
            where T1 : UnityEngine.Object
            where T2 : UnityEngine.Object
            where T3 : UnityEngine.Object
            where T4 : UnityEngine.Object
            where T5 : UnityEngine.Object
            where T6 : UnityEngine.Object
            where T7 : UnityEngine.Object
        {
            Dispose(ref o1, t);
            Dispose(ref o2, t);
            Dispose(ref o3, t);
            Dispose(ref o4, t);
            Dispose(ref o5, t);
            Dispose(ref o6, t);
            Dispose(ref o7, t);
        }

#endif

    }

#ROOTNAMESPACEEND#

リークの可能性のあるクラスを自動検出

UnityEngine.Object を継承した型をクラスのメンバーとして持っていて、OnDestroy もしくは Dispose メソッドが無いモノを探し出すエディター拡張を使って検出する。

LMS それ自体はさほどメモリを消費しないが、クラスメンバーとして大きなサイズのバイト列や JSON 文字列などを持っている場合、LMS の発生に伴ってそれらのメモリも解放されないことになるので特に注意したい。

LMS (Leaked Managed Shell) 発生条件と対処方法

エディター拡張を使って一覧してみると、基底クラス(Component)が持っている transformrigidbody その他のフィールドを除いてもかなりの数が該当してしまう。

LMS はプレハブの階層に含まれるスクリプトの参照を握りっぱなしにしたままプレハブを破棄する、そんなロジックの場合に発生しやすいようなので、普段使いでは全く問題は起きないスクリプトが「そういう使われ方」をしたときにはじめて問題を起こす「かもしれない」という非常に厄介なモノ。

機械的に抽出しようとすると文脈を見て判別しなければならないので一筋縄ではいかないし、差し当たっては「そういう使われ方」をするかもしれないからとりあえず OnDestroy Dispose を実装しておく、という方針で対処するしかなさそう。

LeakedManagedShellDetector エディター拡張
  • 根こそぎ探す: メインメニュー > Tools > Detect Leaked Managed Shell Potentials
  • Project パネルで選んだものだけ: 右クリック > Leaked Managed Shell Detector

※ コピペして使えるように、自動生成された Dispose() メソッドのソースコードをログとして出力する

#if UNITY_EDITOR && UNITY_2021_3_OR_NEWER

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

internal static class LeakedManagedShellDetector
{
    const string MENU_MAIN = "Tools/Detect Leaked Managed Shell Potentials";
    const string MENU_ASSETS = "Assets/Leaked Managed Shell Detector";

    readonly static BindingFlags BIND_FLAGS
        = BindingFlags.Public
        | BindingFlags.NonPublic
        | BindingFlags.Static
        | BindingFlags.Instance
        | BindingFlags.GetField | BindingFlags.SetField
        | BindingFlags.GetProperty | BindingFlags.SetProperty
        ;

    // both StartsWith and EndsWith are tested on namespaces and assembly (.dll) names
    readonly static string[] IGNORED_NAMESPACES = new string[]
    {
        "UnityEngine.",
        "UnityEditor.",
        "UnityEditorInternal.",
        "Unity.",
        "TMPro.",
        ".Editor",
        "-Editor",
    };
    readonly static string[] DISPOSE_METHODS = new string[]
    {
        "OnDestroy",
        "Dispose",
    };
    readonly static Type[] IGNORED_TYPES = new Type[]
    {
        //typeof(GameObject),
        //typeof(Transform),
    };

    const string INDENT = "    ";
    const string READONLY = "// readonly member is not supported: ";
    const string METHOD_IMPL = "MethodImpl"; //"global::System.Runtime.CompilerServices.MethodImpl";
    const string METHOD_SIGNATURE = "\n[" + METHOD_IMPL + "(" + METHOD_IMPL + @"Options.AggressiveInlining)]
public void Dispose()
{
" + INDENT + @"//<auto-generated>" + nameof(LeakedManagedShellDetector) + @"</auto-generated>
";
    const string LOG_PREFIX = "// LMS Disposer Generator";
    const string MS_DISPOSE = "ManagedShell.Dispose";


    static bool IsUnityObject(Type t)
    {
        if (t.IsSubclassOf(typeof(UnityEngine.Object)))
        {
            return !IGNORED_TYPES.Contains(t);
        }
        else if (t.IsSubclassOf(typeof(Delegate)))
        {
            return false;
        }
        else if (t.IsArray)
        {
            return IsUnityObject(t.GetElementType());
        }
        else if (t.IsGenericType)
        {
            foreach (var generic in t.GetGenericArguments())
            {
                if (IsUnityObject(generic))
                    return true;
            }
        }
        return false;
    }


    static bool Detect(Type cls, out string generatedDisposerMethod)
    {
        bool isCandidate = false;
        var sb = new StringBuilder(METHOD_SIGNATURE);
        foreach (var member in cls.GetMembers(BIND_FLAGS))
        {
            if (member.DeclaringType == typeof(Component) ||
                member.DeclaringType == typeof(ScriptableObject))
                continue;

            if (member is FieldInfo f && IsUnityObject(f.FieldType))
            {
                isCandidate = true;
                if (f.FieldType.IsSubclassOf(typeof(UnityEngine.Object)))
                {
                    sb.AppendLine($"{INDENT}{(f.IsInitOnly ? READONLY : string.Empty)}{MS_DISPOSE}(ref {f.Name});");
                }
                else
                {
                    sb.AppendLine($"{INDENT}{MS_DISPOSE}({(f.IsInitOnly ? string.Empty : "ref ")}{f.Name});");
                }
                //break;
            }
            else if (member is PropertyInfo p && IsUnityObject(p.PropertyType))
            {
                isCandidate = true;
                if (p.PropertyType.IsSubclassOf(typeof(UnityEngine.Object)))
                {
                    sb.AppendLine($"{INDENT}{(!p.CanWrite ? READONLY : string.Empty)}{MS_DISPOSE}(ref {p.Name});");
                }
                else
                {
                    sb.AppendLine($"{INDENT}{MS_DISPOSE}({(!p.CanWrite ? string.Empty : "ref ")}{p.Name});");
                }
                //break;
            }
        }
        sb.AppendLine("}");
        sb.AppendLine("// End of Disposer for " + cls.FullName);
        sb.AppendLine();

        generatedDisposerMethod = sb.ToString();
        return isCandidate;
    }


    [MenuItem(MENU_ASSETS)]
    static void GenerateDisposerMethodForSelectedAssets(MenuCommand cmd)
    {
        if (Selection.objects == null)
            return;

        foreach (var sel in Selection.objects)
        {
            Type cls = null;
            if (sel is MonoScript mono)
            {
                cls = mono.GetClass();
            }

            if (cls == null)
            {
                UnityEngine.Debug.Log("unsupported type: " + sel.name);
                continue;
            }

            if (Detect(cls, out var disposerMethod))
            {
                UnityEngine.Debug.LogWarning($"{LOG_PREFIX}{disposerMethod}");
            }
            else
            {
                UnityEngine.Debug.Log("No leaks: " + cls.FullName);
            }
        }
    }

    static bool IsIgnored(string name)
    {
        foreach (var ignore in IGNORED_NAMESPACES)
        {
            if (name.StartsWith(ignore, StringComparison.Ordinal)
                || name.EndsWith(ignore, StringComparison.Ordinal))
            {
                return true;
            }
        }
        return false;
    }


    [MenuItem(MENU_MAIN)]
    static void DetectLMSPotentials()
    {
        bool detectOnlyUnityOb = EditorUtility.DisplayDialog(nameof(DetectLMSPotentials),
            "Detect only UnityEngine.Object Inheritances?\n\n * Types defined in Packages or DLL are always ignored.",
            "Yes", "No");

        foreach (var assem in AppDomain.CurrentDomain.GetAssemblies())
        {
            if (IsIgnored(assem.FullName[..assem.FullName.IndexOf(',')]))
                continue;

            foreach (var cls in assem.GetTypes())
            {
                if (detectOnlyUnityOb && !cls.IsSubclassOf(typeof(UnityEngine.Object)))
                    continue;

                if (IsIgnored(Path.GetFileName(cls.FullName)))
                    continue;

                if (!Detect(cls, out var disposerMethod))
                    continue;

                //OnDestroy/Dispose?
                bool isDisposed = false;
                foreach (var method in cls.GetMethods(BIND_FLAGS))
                {
                    if (DISPOSE_METHODS.Contains(method.Name))
                    {
                        isDisposed = true;
                        break;
                    }
                }
                if (isDisposed)
                    continue;

                // dirty code!!
                UnityEngine.Object asset = null;
                GameObject tmpGO = null;
                try
                {
                    if (cls.IsSubclassOf(typeof(MonoBehaviour)))
                    {
                        tmpGO = new GameObject();
                        asset = tmpGO.AddComponent(cls);
                        bool isFound = AssetDatabase.TryGetGUIDAndLocalFileIdentifier<Object>(
                            MonoScript.FromMonoBehaviour(asset as MonoBehaviour), out var GUID, out var localId);
                        var path = AssetDatabase.GUIDToAssetPath(GUID);

                        Object.DestroyImmediate(asset);
                        asset = null;
                        if (isFound
                            && !path.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)
                            && !path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                        {
                            asset = AssetDatabase.LoadMainAssetAtPath(path);
                        }
                        Object.DestroyImmediate(tmpGO);
                        tmpGO = null;

                        if (path.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)
                            || path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                        {
                            //UnityEngine.Debug.LogError("PACKAGE/DLL IGNORED: " + path);
                            continue;
                        }
                    }
                }
                catch
                {
                }
                finally
                {
                    if (tmpGO != null)
                    {
                        Object.DestroyImmediate(tmpGO);
                        tmpGO = null;
                    }
                }

                UnityEngine.Debug.LogWarning(LOG_PREFIX
                    + $" \t {assem.FullName[..assem.FullName.IndexOf(',')]}"
                    + $" \t {cls.FullName}\n"
                    //+ $"//{new string('=', 50)}"
                    + $"{disposerMethod}\n"
                    , asset);
            }
        }
    }

}

#endif

付録

プロジェクト全体から .Destroy .DestroyImmediate を検索するための正規表現は 👇

  • [\.\s]+(Destroy|DestroyImmediate)\s*\(

--

この問題の発端は Unity で C# と JavaScript が使えた時代に JavaScript 的な if (obj) ヌルチェックが出来るようにしたかったからだろうね。

ユーザー目線だと今となっては ?. ?? ??= は使えるけど意図した動作をしたりしなかったりするし、もう直しちゃっても良いと思うけどね。公式のサンプルとかアセットストアで販売されている古いアセットが動かなくなると面倒だから難しいのだろうけど。

代わりに Unity のアナライザーに Leaked Managed Shell の検出を実装して欲しいところだけど、機械的に判別するのは面倒そう。とりあえず Destroy DestroyImmediate の全てを対象に警告やエラーを設定できるようにしてくれるだけでも助かるんだけどなー。

BannedApiAnalyzers なんてのもあるけどちょっと使うのが面倒なんだ。

--

以上です。お疲れ様でした。

10
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?