【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への検索が無くなったことにより、実行速度が大幅に改善します。