はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】UnityEngine.Scripting.PreserveAttributeを用いてストリッピングを防止する(実は独自PreserveAttributeでも対応可)

はじめに

今回はPreserveAttribute属性を用いてストリッピングを防止する方法について紹介したいと思います。
docs.unity3d.com

概要

まずはコードストリッピングについて概要を載せておきます。

マネージドコードのストリッピングはビルドから未使用のコードを削除し、最終ビルドサイズを大幅に削減します。
マネージコードストリッピングは、プロジェクトの C# スクリプトからビルドされたアセンブリ、パッケージとプラグインの一部であるアセンブリ、.NET Framework のアセンブリなどのマネージアセンブリからコードを削除します。

マネージコードストリッピング - Unity マニュアル

しかしリフレクションを利用した際に、削除されたくないコードが削除されてしまうといった問題がよく発生してしまいます。(特にDI周り)

その対策としてUnityではPreserveAttributeというものが存在し、属性をつけることでコードストリッピングを防止することができます。

PreserveAttribute はクラス、メソッド、フィールド、プロパティーを削除することでバイトコードのストリッピングを防止します。

When you create a build, Unity will try to strip unused code from your project. This is great to get small builds. However, sometimes you want some code to not be stripped, even if it looks like it is not used. This can happen for instance if you use reflection to call a method, or instantiate an object of a certain class. You can apply the [Preserve] attribute to classes, methods, fields and properties. In addition to using PreserveAttribute, you can also use the traditional method of a link.xml file to tell the linker to not remove things. PreserveAttribute and link.xml work for both the Mono and IL2CPP scripting backends.

// DeepL翻訳
PreserveAttribute はクラス、メソッド、フィールド、プロパティーを削除することでバイトコードのストリッピングを防止します。
ビルドを作成するとき、Unityはプロジェクトから未使用のコードを取り除こうとします。これは小さなビルドを作成するのには最適です。しかし、たとえ使われていないように見えても、いくつかのコードを削除したくない場合があります。例えば、リフレクションを使ってメソッドを呼び出したり、特定のクラスのオブジェクトをインスタンス化したりする場合です。[Preserve]属性は、クラス、メソッド、フィールド、プロパティに適用できます。PreserveAttributeを使用するだけでなく、link.xmlファイルという伝統的な方法を使用して、リンカーに削除しないように指示することもできます。PreserveAttributeとlink.xmlは、MonoとIL2CPPの両方のスクリプト・バックエンドで動作します。

Scripting.PreserveAttribute - Unity スクリプトリファレンス

使い方

[Preserve]をクラス・メソッド・フィールド・プロパティにつけることで、コードストリッピングを防止できます。

public class Test : MonoBehaviour
{
    private void Start()
    {
        typeof(Test).GetMethod("Hoge", BindingFlags.NonPublic | BindingFlags.Static)?.Invoke(null, null);
    }

    // リフレクションで利用されていることをコンパイル時点で検知できないので、誤ってコードストリッピング(削除)されてしまう
    [Preserve]
    private static void Hoge()
    {
        Debug.Log("Hoge!");
    }
}

よくある例として、リフレクションでしか利用していないメソッドが誤ってコードストリッピングされてしまうことがあります。特にDI周りですね。そんな場合に[Preserve]を利用すると良いです。

実験

本当に未使用なコードが削除されるか確かめてみましょう。

実際に適当なメソッドを用意してみてビルドして試してみます。またScripting BackendMonoを利用し、Managed Stripping LevelHighに設定しています。

// 実際のコード
namespace Main
{
    public class Sample : MonoBehaviour
    {
        [Preserve]
        public void HogeWithPreserve()
        {
            
        }

        public void HogeWithNoPreserve()
        {
        
        }
    }   
}

ビルドした後のDllを逆コンパイルして中身を調べてみます。

// 逆コンパイルしたコード
namespace Main
{
  public class Sample : MonoBehaviour
  {
    [Preserve]
    public void HogeWithPreserve()
    {
    }
  }
}

[Preserve]を付けていないメソッドはコードストリッピングされ、付けていたメソッドが残っていることが確認できます。

UnityEngine.dllに依存しないために

For 3rd party libraries that do not want to take on a dependency on UnityEngine.dll, it is also possible to define their own PreserveAttribute. The code stripper will respect that too, and it will consider any attribute with the exact name "PreserveAttribute" as a reason not to strip the thing it is applied on, regardless of the namespace or assembly of the attribute.

// DeepL翻訳
UnityEngine.dllに依存したくないサードパーティライブラリについては、独自のPreserveAttributeを定義することも可能です。コードストリッパーはそれを尊重し、アトリビュートの名前空間やアセンブリに関係なく、"PreserveAttribute "という正確な名前を持つアトリビュートを、それが適用されるものをストリップしない理由として考慮します。

Scripting.PreserveAttribute - Unity スクリプトリファレンス

例えば以下のような独自の属性を定義してみます。

// 実際のコード
using System;
using UnityEngine;

namespace Main
{
    public class Sample : MonoBehaviour
    {
        [Preserve]
        public void HogeWithPreserve()
        {
            
        }

        public void HogeWithNoPreserve()
        {
        
        }
    }

    public class PreserveAttribute : Attribute
    {
        public PreserveAttribute() { }
    }
}

UnityEngine.Scripting.PreserveAttributeを利用していませんが、どうなるのか確認してみましょう。

// 逆コンパイルしたコード
namespace Main
{
  public class Sample : MonoBehaviour
  {
    [Preserve]
    public void HogeWithPreserve()
    {
    }
  }
}
// 逆コンパイルしたコード
namespace Main
{
  public class PreserveAttribute : Attribute
  {
  }
}

しっかりと[Preserve]の役割を果たしてくれていました。