在构建过程中,Unity 会通过称为托管代码剥离的过程删除未使用或无法访问的代码,这能显著减小应用程序的最终大小。托管代码剥离将从托管程序集(包括从项目中的 C# 脚本构建的程序集、包含在包和插件中的程序集以及 .NET 框架中的程序集)中删除代码。
Unity 使用一个名为 Unity 链接器的工具对项目程序集的代码执行静态分析。静态分析会识别出在执行过程中无法访问的任何类、类的部分、函数或函数的部分。此分析仅涵盖在构建时已存在的代码,因为 Unity 执行静态分析时,运行时生成的代码还不存在。
您可以使用托管剥离级别 (Managed Stripping Level) 设置来配置 Unity 为项目执行的代码剥离级别。要防止 Unity 删除代码的特定部分,请使用注释来指示 Unity 链接器应保留代码库中的哪些部分。想了解更多相关信息,请参阅 Unity linker。
托管剥离级别 (Managed Stripping Level) 属性决定了 Unity 链接器在分析和剥离应用程序代码时遵循的规则集。当将设置从最低 (Minimal) 增加到高 (High) 时,规则使链接器能够在更多程序集内搜索无法访问的代码。Unity 链接器在较高的设置下会移除更多代码,这可以减小最终构建包的大小,但搜索范围的扩大意味着每次构建所需的时间会更长。
如要更改托管剥离级别 (Managed Stripping Level) 属性:
| 属性: | 功能: |
|---|---|
| Disabled | Unity 不会删除任何代码。 仅当使用 Mono 脚本后端时,此设置才可见,并且为默认设置。 |
| Minimal | Unity 仅在 UnityEngine 和 .NET 类库中搜索未使用的代码。Unity 不会删除任何用户编写的代码。此设置最不可能导致任何意外的运行时行为。 对于那些可用性比构建包大小更重要的项目来说,此设置很有用。如果使用 IL2CPP 脚本后端,这是默认设置。 |
| Low | Unity 会搜索一些用户编写的程序集以及所有 UnityEngine 和 .NET 类库以查找未使用的代码。此设置应用了一组规则,这些规则会移除部分未使用的代码,同时尽可能降低出现意外情况的可能性,比如使用反射的运行时代码在行为上发生变化。 |
| Medium | Unity 会对所有程序集进行部分搜索,以找出无法访问的代码。该设置应用了一组规则,会剥离更多类型的代码模式,从而减小构建包的大小。虽然 Unity 不会剥离所有可能的无法访问的代码,但此设置确实会增加出现不良或意外行为变化的风险。 |
| High | Unity 会对所有程序集进行全面搜索,以找出无法访问的代码。在此设置下,Unity 更优先考虑减小构建包大小,而非代码稳定性,会尽可能多地移除代码。 与较低的剥离级别相比,这种搜索所需的时间要长得多。仅在构建包大小极为关键的项目中使用此设置。彻底测试应用程序并谨慎使用 [Preserve] 属性和 link.xml 文件,以确保 Unity 链接器不会剥离重要代码。 |
您可以使用注释来防止 Unity 链接器剥离代码的特定部分。如果应用程序生成的运行时代码在 Unity 执行静态分析时不存在(例如通过反射),此功能会很实用。注释既可以为 Unity 链接器提供通用指导,告知其不应剥离哪些代码模式;也可以给出指令,要求保留特定的已定义代码段。
若要让代码在托管代码剥离过程中不被移除,您可以使用两种常用方法来注释代码:
上述每种技术都可以更好地控制 Unity 链接器在较高剥离级别下剥离的代码量,和减少重要代码被剥离的几率。当代码通过反射引用其他代码时,注释特别有用,因为 Unity 链接器不一定总能够检测到反射的使用情况。
保留那些使用反射或在运行时生成其他代码的代码部分,能够大大降低执行应用程序时出现意外行为的可能性。有关 Unity 链接器可以识别的反射模式的示例,请参阅 Unity 中间语言链接器反射测试套件。
根注释会强制 Unity 链接器将代码元素视为根元素,这种元素在代码剥离过程中不会剥离。根据需要保留单个类型及其构造函数,还是保留程序集,您可以使用两种类型的根注释:
使用 Preserve 属性可以从 Unity 链接器的静态分析中单独排除代码的特定部分。要使用此属性注释一段代码,请在要保留的代码第一部分之前添加 [Preserve]。下面的列表描述了在对不同的代码元素应用 [Preserve] 属性时 Unity 链接器会保留哪些实体:
[Preserve] 属性,请将该属性声明放在程序集包含的任何 C# 文件中,但需在所有命名空间声明之前。[add] 访问器和 [remove] 访问器。如果要保留类型及其默认构造函数,请使用 [Preserve] 属性。如果要保留其中一个而不是同时保留这两个,请使用 link.xml 文件。
可以在任何程序集中和任何命名空间中定义 [Preserve] 属性。因此,可以使用 UnityEngine.Scripting.PreserveAttribute 类、为其创建子类或创建您自己的 PreserveAttribute。例如:
class Foo
{
[Preserve]
public void PreservedMethod(){}
}
您可以在项目中包含一个名为 link.xml 的 .xml 文件,以保留一系列特定程序集或程序集部分。link.xml 文件必须位于项目的 Assets 文件夹或 Assets 文件夹的子目录中,并且必须在文件中包含 <linker> 标签。Unity 链接器会将 link.xml 文件中保留的任何程序集、类型或成员都视为根类型。
您可以在项目中使用任意数量的 link.xml 文件。因此,您可以为每个插件提供单独的保留声明。软件包中不能包含 link.xml 文件,但可以从非软件包 link.xml 文件中引用软件包程序集。
以下示例说明了使用 link.xml 文件声明项目程序集的根类型时可以采用的不同方式:
<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, its backing field (if present),
getter, and setter methods-->
<property signature="System.Int32 PropertyName" />
<property signature="System.Int32 PropertyName" accessors="all" />
<!--Preserve a property, its backing field (if present), and getter method-->
<property signature="System.Int32 PropertyName" accessors="get" />
<!--Preserve a property, its 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, its 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>
接下来的示例展示了如何声明整个程序集:
<!--Preserve an entire assembly-->
<assembly fullname="AssemblyName" preserve="all"/>
<!--No "preserve" attribute and no types specified means preserve all-->
<assembly fullname="AssemblyName"/>
<!--Fully qualified assembly name-->
<assembly fullname="AssemblyName, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
<type fullname="AssemblyName.Foo" preserve="all"/>
</assembly>
<!--Force an assembly to be processed for roots but don’t explicitly preserve anything in particular. Useful when the assembly isn't referenced.-->
<assembly fullname="AssemblyName" preserve="nothing"/>
以下示例展示了如何保留嵌套类型或泛型类型:
<!--Examples with generics-->
<type fullname="AssemblyName.G`1">
<!--Preserve a field with generics in the signature-->
<field signature="System.Collections.Generic.List`1<System.Int32> FieldName" />
<field signature="System.Collections.Generic.List`1<T> FieldName" />
<!--Preserve a method with generics in the signature-->
<method signature="System.Void MethodName(System.Collections.Generic.List`1<System.Int32>)" />
<!--Preserve an event with generics in the signature-->
<event signature="System.EventHandler`1<System.EventArgs> EventName" />
</type>
<!--Preserve a nested type-->
<type fullname="AssemblyName.H/Nested" preserve="all"/>
<!--Preserve all fields of a type if the type is used. If the type isn't used, it will be removed-->
<type fullname="AssemblyName.I" preserve="fields" required="0"/>
<!--Preserve all methods of a type if the type is used. If the type isn't used, it will be removed-->
<type fullname="AssemblyName.J" preserve="methods" required="0"/>
<!--Preserve all types in a namespace-->
<type fullname="AssemblyName.SomeNamespace*" />
<!--Preserve all types with a common prefix in their name-->
<type fullname="Prefix*" />
link.xml 文件的 <assembly> 元素有三个特殊用途属性,您可以启用这些属性对注释进行更精细的控制。
ignoreIfMissing:如果需要为一个并非在所有播放器构建期间都存在的程序集声明保留,请使用此属性。<linker>
<assembly fullname="Foo" ignoreIfMissing="1">
<type name="TypeName"/>
</assembly>
</linker>
ignoreIfUnreferenced:在某些情况下,您可能希望仅当程序集被其他程序集引用时才保留程序集中的实体。仅当一个程序集中至少有一个类型被引用时,使用此属性来保留该程序集中的实体。<linker>
<assembly fullname="Bar" ignoreIfUnreferenced="1">
<type name="TypeName"/>
</assembly>
</linker>
windowsruntime: 为 Windows 运行时元数据 (.winmd) 程序集定义保留时,必须在 link.xml 文件中的 <assembly> 元素中添加 windowsruntime 属性:<linker>
<assembly fullname="Windows" windowsruntime="true">
<type name="TypeName"/>
</assembly>
</linker>
依赖关系注释定义了各种代码元素之间的依赖关系。这些注释可用于保留 Unity 链接器无法静态分析的代码模式,例如反射。这些注释还确保在没有根元素使用这些代码元素的情况下,它们不会被错误地保留下来。可以使用两种方法来更改 Unity 链接器处理代码元素的方式:
[Preserve] 属性适用于始终需要 API 的情况。其他属性则可用于更通用的代码保留场景。例如,您可以通过使用 RequireImplementorsAttribute 对接口进行注释,来保留实现该特定接口的所有类型。
要注释特定的编码模式,请使用以下一个或多个属性:
您可以通过各种方式组合这些属性,从而更精确地控制 Unity 链接器如何保留代码。
[assembly: UnityEngine.Scripting.AlwaysLinkAssembly] 属性强制 Unity 链接器搜索程序集,无论该程序集是否被构建中包含的另一个程序集引用。您只能将 AlwaysLinkAssembly 属性应用于程序集。
该属性并不会直接保留程序集中的代码。相反,该属性指示 Unity 链接器对该程序集应用根标记规则。如果程序集中没有代码元素符合根标记规则,Unity 链接器仍会将该程序集从构建中移除。
可在包含一个或多个方法的预编译程序集或包程序集上将此属性与 [RuntimeInitializeOnLoadMethod] 属性结合使用,但后者可能不包含在项目的任何场景中直接或间接使用的类型。
如果程序集定义 [assembly: AlwaysLinkAssembly] 属性并由构建中包含的其他程序集所引用,则该属性对于输出没有任何影响。