神様は有休消化中です。

Unity関連の技術ネタを書いてます。

【Unity】Reflectionを使った情報収集を軽量化するアイデア

ライブラリを開発している中で、書き心地を担保するためにReflectionを使用したい場面が出てきます。
例えば特定のキーが押された時に指定した関数が実行されるようなショートカットキーの仕組みを実装する場合、Attributeでショートカットアクションを指定出来るようになっていると便利です。

[Shortcut("ファイル保存", KeyCode.LeftControl, KeyCode.S)]
static void OnSave()
{
    // 保存処理
}

このような作りにしようとした場合、ライブラリの初期化処理で以下のようなコードを書いてショートカットアクションを収集することになります。

void CollectShortcutActions()
{
    // アプリ内の全てのアセンブリを取得
    foreach(var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
    {
        // すべての型を取得
        foreach(var type in assembly.GetTypes())
        {
            // 型の中からprivate static, public static関数を取得
            foreach(var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
            {
                // [Shortcut]が指定されているかを判定
                var attr = method.GetCustomAttributes<ShortcutAttribute>();
                if (attr == null)
                    continue;

                // ショートカットアクションの登録処理
            }
        }
    }
}

見るからに重そうですね。
Reflectionを使った処理はもちろんオーバーヘッドの高いものですが、アプリ内の全てのAssemblyに対して処理を行っているため、Unityエンジンの持つクラス群やこのAttributeを使っていないAssemblyに対しても検索処理が行われ、かなりの処理時間がかかってしまっています。
今回はこの処理をなるべく軽くするためのアイデアを共有します。

AttributeのAssemblyを分ける

まず初めに、収集したいAttributeクラスを別Assemblyへ切り分けます。
UnityにはAssembly Definition Assetという仕組みがあり、指定したフォルダ以下のコードを別のAssemblyへ切り分けることができます。
Assembly Definition Assetについては、公式マニュアルこちらのページを参照してください。

対象のAssemblyが、AttributeのAssemblyに依存しているかどうかをチェックする

当然ですが、Attributeを使用しているクラスが含まれるAssemblyは、Attributeが定義されたAssemblyを参照しています。 例えばShortcutAttributeを収集したいと考えたときは、ShortcutAttributeを定義したAssemblyを参照しているものだけを検索すればよいことになります。
C#では、AssemblyがどのAssemblyを参照しているかを取得するAPIが用意されています。
これを使って、ショートカットアクションの収集処理を以下のように書き換えます。

void CollectShortcutActions()
{
    foreach(var method in GetAllMethodWithAttribute<ShortcutAttribute>())
    {
        // ショートカットアクションの登録処理
    }
}

IEnumerator<MethodInfo> GetAllMethodWithAttribute<T>() where T : System.Attribute
{
    var targetAssemblyName = typeof(T).Assembly.GetName().FullName;

    foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
    {
        var referencedAssemblies = assembly.GetReferencedAssemblies();
        var isTarget = assembly.GetName().FullName == targetAssemblyName;
        if (!isTarget)
        {
            for (int i = 0; i < referencedAssemblies.Length; i++)
            {
                var assemblyInfo = referencedAssemblies[i];
                if (assemblyInfo.FullName == targetAssemblyName)
                {
                    isTarget = true;
                    break;
                }
            }
        }

        if (!isTarget)
            continue;

        foreach (var type in assembly.GetTypes())
        {
            foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
            { 
                var attr = method.GetCustomAttributes<T>();
                if (attr == null)
                    continue;

                yield return method;
            }
        }
    }
}

これで、Unityエンジンのクラス群や関係のないAssemblyへの検索が無くなったことにより、実行速度が大幅に改善します。