Unity Managed Code Stripping with Linker

After introducing a new plugin into the project this week, local debugging is normal, but some files will be lost when a large package comes out. After checking for a long time, I found that it is because the file is not directly referenced, and it is through’ScriptableObject. CreateInstance (typeName) 'This form was dynamically created, so it was finally packed and removed by the code clipping process.

The solution is also relatively simple, add a link.xml in the Assets directory, and then declare that the assembly cannot be trimmed.

But looking back, we still need to take a look at the logic of this code clipping and why link.xml can avoid code clipping.

Managed code stripping

During the build process, Unity removes unused or inaccessible code through managed code stripping, which can significantly reduce the size of the final product. Managed code stripping removes code from a managed assembly, which includes the assembly where the C #code in the project is located, the assembly where the plugin or third-party package is located, and the assembly where the .NET Framework is located.

Unity uses a tool called Unity Linker to perform static code inspection. Static analysis can identify any class, part of a class, part of a function, or part of a function that cannot be reached during execution. This inspection will only be performed on code that exists at build time, and code generated dynamically at runtime will not be detected.

Configure code clipping

We can control the degree of tailoring of Unity executable code by using’Managed Stripping Level '. In order to avoid Unity removing certain parts of the code we specifically specify, we can use some kind of comment to declare which parts should be retained by Unity Linker.

We can modify the level of code clipping in Player Settings. The optional values are as follows:

  • Disabled: Unity does not do any code clipping. This configuration can only be selected when we use Mono, and is the default configuration
  • Minimal: Unity will only detect UnityEngine and .NET code. No user code will be removed. This configuration is least likely to cause unexpected runtime performance. This configuration is mostly used in products where usability priority is higher than package size. This configuration is the default configuration for IL2CPP
  • Low: Unity searches for some user assemblies and all UnityEngine and .NET code. This setting applies a set of rules that remove some unused code, but minimizes the possibility of unintended consequences, such as changes in the behavior of runtime code that uses reflection.
  • Medium: Unity partially searches all assemblies for inaccessible code. This setting applies a set of rules that remove more types of code patterns to reduce build size. Although Unity does not remove all possible inaccessible code, this setting does increase the risk of unwanted or unexpected behavior changes.
  • High: Unity performs the most extensive inspection of all assemblies, Unity prioritizes reducing code size over code stability, and removes as much code as possible.

Preserve code through annotations

We can use annotations to prevent the Unity Linker from tailoring parts of the code we make. This is useful when our application generates runtime code, such as when we use reflection in our code. Comments either provide general guidance to the Unity linker, telling it which code patterns should not be removed, or telling it not to remove specific, defined code segments

There are two main ways to annotate code to save it during the managed code stripping process:

  • Root annotations identify parts of the code as root. The Unity linker does not remove any code marked as root. Root annotations are less complicated to use, but also cause the Unity linker to keep some code that should be removed
  • Annotations define the connections between code elements. Compared to root annotations, dependency annotations can reduce the amount of oversaved code

Each of these techniques provides more control over the amount of code the Unity linker strips at higher stripping levels and reduces the chance of important code being stripped. Comments are especially useful when your code references other code through reflection, as the Unity linker cannot always detect the use of reflection.

Root

Root comments force Unity Linker to treat the code element as the root element, so that code changes will not be cut.

There are two ways to mark a piece of code as a root element, and the choice of two ways depends on whether you want to keep the constructor function of the assembly or a single type

  • Preserve Attribute: Preserve a single type as the root element
  • Link.xml: holds the entire assembly as root, as well as other assemblies referenced by the modified assembly

Use the Preserve attribute to mark the root

Use the Preserve property to individually exclude specific parts of code from the static analysis of the Unity linker. To annotate code snippets with this property, add [Preserve] immediately before the first part of the code you want to keep. The following list describes the entities that the Unity linker keeps when you annotate different code elements with the [Preserve] property

  • Assembly: Preserves all types that are used and defined in the assembly. To assign the [Preserve] attribute to an assembly, place the attribute declaration in any C# file included in the assembly, before any namespace declarations.
  • Type: Preserves a class or type and its default constructor.
  • Method: Preserves the method, the type that declares the method, the type the method returns, and the types of all of its arguments.
  • Property: Preserves the property, the type that declares the property, the value type of the property, and methods that get and set the property value.
  • Field: Preserves the field, the field type, and the type that declares the field.
  • Event: Preserves the event, the type that declares the event, type, the type the event returns, the [add] accessor, and the [remove] accessor.
  • Delegate: Preserves the delegate type and all methods that the delegate invokes.

If you want to keep both a type and its default constructor, you can use the [Preserve] property. If you want to keep one of them, but not both, use the link.xml file

Use XML files to mark

An xml file named link.xml can be used in a project to keep a list of specific assemblies or parts of assemblies. The Xml file must appear in the Assets folder or a subdirectory of the Assets folder in the project and must include the ‘< linker >’ tag in the file. The Unity linker treats any assemblies, types, or members saved in the link.xml file as root types.

You can use any number of link.xml files in your project. Therefore, you can provide a separate save declaration for each plug-in. You cannot include a link.xml file in a Package, but you can reference a Package assembly from a non-Package link.xml file.

Below is an official example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<linker>
<!--Preserve types and members in an assembly-->
<assembly fullname="AssemblyName">
<!--Preserve an entire type-->
<type fullname="AssemblyName.MethodName" preserve="all"/>

<!--No "preserve" attribute and no members specified means preserve all members-->
<type fullname="AssemblyName.MethodName"/>

<!--Preserve all fields on a type-->
<type fullname="AssemblyName.MethodName" preserve="fields"/>

<!--Preserve all fields on a type-->
<type fullname="AssemblyName.MethodName" preserve="methods"/>

<!--Preserve the type only-->
<type fullname="AssemblyName.MethodName" preserve="nothing"/>

<!--Preserving only specific members of a type-->
<type fullname="AssemblyName.MethodName">

<!--Fields-->
<field signature="System.Int32 FieldName" />

<!--Preserve a field by name rather than signature-->
<field name="FieldName" />

<!--Methods-->
<method signature="System.Void MethodName()" />

<!--Preserve a method with parameters-->
<method signature="System.Void MethodName(System.Int32,System.String)" />

<!--Preserve a method by name rather than signature-->
<method name="MethodName" />

<!--Properties-->

<!--Preserve a property, it's backing field (if present),
getter, and setter methods-->
<property signature="System.Int32 PropertyName" />

<property signature="System.Int32 PropertyName" accessors="all" />

<!--Preserve a property, it's backing field (if present), and getter method-->
<property signature="System.Int32 PropertyName" accessors="get" />

<!--Preserve a property, it's backing field (if present), and setter method-->
<property signature="System.Int32 PropertyName" accessors="set" />

<!--Preserve a property by name rather than signature-->
<property name="PropertyName" />

<!--Events-->

<!--Preserve an event, it's backing field (if present), add, and remove methods-->
<event signature="System.EventHandler EventName" />

<!--Preserve an event by name rather than signature-->
<event name="EventName" />

</type>
</assembly>
</linker>

Properties of XML

The ‘< assembly >’ tag of the link.xml file has three attributes that allow us to control the code tailoring more finely

  • ignoreIfMissing: Use this attribute if you need to declare preservations for an assembly that doesn’t exist during all Player builds.
  • ignoreIfUnreferenced: In some cases, you might want to preserve entities in an assembly only when that assembly is referenced by another assembly. Use this attribute to preserve the entities in an assembly only when at least one type is referenced in an assembly.
  • windowsruntime: When you define preservations for a Windows Runtime Metadata (.winmd) assembly, you must add the windowsruntime attribute to the <assembly>element in the link.xml file:

Dependency

Dependency comments define the dependencies between different code elements. These comments are useful for preserving code patterns (such as reflection) that the Unity linker cannot statically analyze. These comments also ensure that these code elements are not mistakenly retained when no root element is used. There are two methods that can be used to change the way the Unity linker handles code elements:

  • Annotation attributes: These attributes indicate that the Unity linker should retain specific code patterns, such as any type derived from an annotation type.
  • AlwaysLinkAssemblyAttribute: Use this attribute to indicate that the Unity linker should process an assembly, even if it is not referenced by any other assemblies in the project.

Annotation

[ Preserve ]属性对于总是需要 API 的情况非常有用。其他属性可以用于更一般的保存。例如,您可以通过使用 RequreComplementorsAttribute 对接口进行注释来保留实现特定接口的所有类型。

To annotate a specific encoding mode, please use one or more of the following attributes:

  • RequireImplementorsAttribute: marks all types that implement this interface as dependencies.
  • RequireDerivedAttribute: marks all types that derive from this type as dependencies.
  • RequiredInterfaceAttribute: marks interface implementations on types as dependencies.
  • RequiredMemberAttribute: marks all members of a type as dependencies.
  • RequireAttributeUsagesAttribute: marks custom attributes as dependencies.

AlwaysLinkAssembly

[ AlwaysLinkAssembly ]属性强制 Unity 链接器搜索程序集,而不管该程序集是否被生成中包含的另一个程序集引用。只能将 AlwaysLinkAssembly 属性应用于程序集。

This property does not directly save the code within the assembly. Instead, this property instructs the Unity linker to apply the root markup rules to the assembly. ** If no code element matches the assembly’s root markup rules, the Unity linker will still remove the assembly from the build **.

Use this property for precompiled or package assemblies that contain one or more types that have the RuntimeInitializeOnLoadMethod property, but may not contain types that are used directly or indirectly in any scenario

Unity

The Unity build process uses a tool called the Unity linker to strip away managed code. The Unity linker is a version of the IL linker customized for Unity. Specific parts of the Unity linker for the custom Unity engine are not publicly available.

The Unity linker is responsible for part of the managed code stripping and engine code stripping processes, which are a separate process provided through IL2CPP.

How the linker works

The Unity linker analyzes all assemblies in the project. First, it marks the root type, methods, properties, and fields. For example, a MonoBehavior derived class added to GameObjects is the root type in one scenario. The Unity linker then analyzes the roots it marks for identification, and marks any managed code that those roots depend on. After completing this static analysis, any remaining unmarked code cannot be accessed through any execution path of the application code, and the Unity linker removes it from the assembly.

The Unity editor creates a list of assemblies with the types used in any scene of the Unity project and passes that list to the Unity linker. The Unity linker then processes those assemblies, any references to those assemblies, any assemblies declared in the link.xml file, and any assemblies with the AlwaysLinkAssembly property. Typically, the Unity linker does not process assemblies included in the project that do not fall into those categories and excludes them from the player build.

For each assembly processed by the Unity linker, it follows a set of rules based on the assembly classification, whether the assembly contains the type used in the scene, and the level of managed stripping selected for the build.

For the purposes of this rule, assemblies fall into the following categories:

  • .NET Class Library assemblies — Includes the Mono class libraries such as mscorlib.dll and System.dll, as well as .NET class library facade assemblies like netstandard.dll.
  • Platform SDK assemblies — Includes the managed assemblies specific to a platform SDK. For example, the windows.winmd assembly that is part of the Universal Windows Platform SDK.
  • Unity Engine Module assemblies — Includes the managed assemblies that make up the Unity Engine, such as UnityEngine.Core.dll.
  • Project assemblies — Includes the assemblies specific to a project such as:
    • Script assemblies such as Assembly-CSharp.dll
    • Precompiled assemblies
    • Assembly Definition Assemblies
    • Package assemblies

When you build a project in Unity, the build process compiles your C # code into the .NET bytecode format, called the Common Intermediate Language (CIL). Unity packages this CIL bytecode into a file called an assembly. The .NET Framework libraries and any C # libraries used in plug-ins in the project are also pre-packaged as assemblies of CIL bytecode.

When the Unity linker performs its static analysis, it follows a set of rules to determine which parts of the CIL bytecode are marked by the Unity linker as necessary for a build. Root Annotations rules determine how the Unity linker identifies and preserves top-level assemblies in a build. Dependency Annotations rules determine how the Unity linker identifies and preserves any code that the root assembly depends on.

The specific code will be used as root and which code will be used as dependencies can be seen in the official doc: https://docs.unity3d.com/Manual/unity-linker.html

Link.xml file features

XML files support a less commonly used “attribute” XML attribute. In this case, the mscolib.xml file embedded in mscolib.dll uses this attribute, but you can use it in any link.xml file when appropriate.

When you use the advanced stripping level, the Unity linker excludes the retention of features that are not supported based on the current version settings.

  • Remoting - Excluding when targeting IL2CPP script backend
  • Sre-exclude when targeting IL2CPP script backend.
  • COM - Exclude when targeting platforms that do not support COM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<linker>

<assembly fullname="Foo">

<type fullname="Type1">

<!--Preserve FeatureOne on platforms that support COM-->

<method signature="System.Void FeatureOne()" feature="com"/>

<!--Preserve FeatureTwo on all platforms-->

<method signature="System.Void FeatureTwo()"/>

</type>

</assembly>

</linker>

Modify the code of Method

When you use the High stripping level, the Unity linker edits the method body to further reduce the code size. This section summarizes some of the noteworthy edits that the Unity linker makes to the method body.

Unity Linker only edits the method body in the .NET library assembly. After editing the method body, the source code of the assembly no longer matches the compiled code in the assembly, which can increase the difficulty of debugging.

The following list describes what the Unity linker can do to edit a method body:

  • Remove unreachable branches - Unity linker removes the If statement block that checks System. environment. platform, and the current target platform is inaccessible.
  • Internal connection methods that only access the field - The Unity linker replaces calls to methods that get or set the field, which have direct access to the field. This often makes it possible to remove the method completely. When you use a Mono backend, the Unity linker only makes this change if it allows the caller of the method to directly access the field based on its visibility. For IL2CPP, the visibility rules do not apply, so the Unity linker makes this change where appropriate.
  • internal connection method that returns constant values - joint debugging within the Unity linker uses a method that only returns constant values.
  • Remove empty methods that do not return calls - The Unity linker removes calls to empty methods that return type void.
  • Remove empty scope - The Unity linker deletes the Try/Finally block when the Last block is empty. Deleting the air conditioner creates an empty Finally block. If this happens during method editing, the Unity linker will delete the entire Try/Finally block. One scenario where this could happen is if the compiler generates a Try/Finally block in a foreach loop in order to call Dispose ().