托管代码剥离将从构建中删除未使用的代码,从而可以显著减小最终构建大小。使用 IL2CPP 脚本后端时,托管代码剥离还可以减少构建时间,因为需要转换为 C++ 并进行编译的代码减少。托管代码剥离将从托管程序集(包括从项目中的 C# 脚本构建的程序集、包含在包和插件中的程序集以及 .NET 框架中的程序集)中删除代码。
托管代码剥离的工作方式是对项目中的代码进行静态分析,检测出在执行过程中永远无法访问的类、类成员甚至函数的某些部分。可以通过 Player Settings 窗口中的 Managed Stripping Level 设置(在 Optimization 部分)来控制 Unity 删除无法访问的代码的激进程度。
__重要信息:__当代码(或插件中的代码)使用反射来动态查找类或成员时,代码剥离工具不能总是检测出项目是否正在使用这些类或成员,因此可能会删除它们。要声明某个项目正在使用这样的代码,请使用 link.xml 文件或 Preserve 属性。
使用项目的 Player Settings 中的 Managed Stripping Level 选项来控制 Unity 删除未使用代码的激进程度。
注意:__此选项的默认值根据 Scripting Backend__ 的当前设置而不同。
属性 | 功能 |
---|---|
Disabled | 不删除代码。 此选项是 Mono 脚本后端的默认剥离级别。当选择 IL2CPP 脚本后端时,由于其对构建时间的影响,Disabled 选项不可用。托管代码越多意味着 IL2CPP 要生成的 C++ 代码越多,也意味着需要编译的 C++ 代码越多。导致的结果是,从进行代码更改到看到更改生效所间隔的时间大幅增加。 |
Low | 根据一组保守的规则删除代码,因此应该会删除大多数无法访问的代码,同时尽量降低实际使用的代码被剥离的可能性。剥离级别低有利于确保可用性,但不利于减小代码大小。 此选项是 IL2CPP 的默认剥离级别(并且已用于 Unity Editor 的许多发行版)。 |
Medium | 根据一组规则删除代码,确保在低 (Low) 剥离级别和高 (High) 剥离级别之间取得平衡。中等 (Medium) 剥离级别不像低剥离级别那么谨慎,也不如高剥离级别那么极端。因此,因删除代码而导致意外副作用的风险大于低剥离级别,但小于高剥离级别。 如果将中等剥离级别用于 IL2CPP 脚本后端,可进一步缩短代码更改与测试之间的迭代时间。 使用 .NET 3.5 脚本运行时版本 (.NET 3.5 Scripting Runtime Version) 设置时,中等剥离级别不可用。 |
High | 尽可能删除无法访问的代码,并生成比中等 (Medium) 剥离级别更小的构建。高 (High) 剥离级别优先考虑减小代码大小,而不是确保可用性;您可能需要添加 link.xml 文件、Preserve 属性或重写有问题的代码部分。 高剥离级别会执行更耗时的分析,以进一步减小大小,因此构建和迭代时间可能比中等剥离级别下的时间更长。 当使用 .NET 3.5 脚本运行时版本 (.NET 3.5 Scripting Runtime Version) 设置时,高剥离级别不可用。 |
注意__:__Managed Stripping Level 选项不会影响删除未使用 Unity 引擎代码的过程(使用 IL2CPP Scripting Backend 设置时可用)。
本部分介绍托管代码剥离的详细信息,以及如何识别和纠正可能出现的任何相关问题。
在 Unity 中构建项目时,构建过程将 C# 代码编译为称为公共中间语言 (CIL) 的 .NET 字节码格式。此 CIL 字节码被打包到称为程序集的文件中。同样,.NET 框架库以及在项目中使用的插件中的所有 C# 库也都会预先打包为 CIL 字节码的程序集。通常,无论项目使用程序集的多少代码,构建过程始终包括整个程序集文件。
托管代码剥离过程将分析项目中的程序集,以查找和删除未实际使用的代码。分析过程使用一组规则来确定要保留的代码和要丢弃的代码。这些规则将在构建大小(包含太多代码)与风险(删除太多代码)之间进行权衡。Managed Stripping Level 设置可用于控制删除代码的激进程度。
Unity 构建过程使用一个名为 UnityLinker 的工具来剥离托管代码。UnityLinker 是 Mono IL Linker 的一个定制版本,专为 Unity 设计。UnityLinker 基于我们的项目分叉,此分叉密切跟踪上游 IL Linker 项目。(请注意,该分叉中未维护 UnityLinker 的 Unity 引擎特有自定义部分。)
UnityLinker 将分析项目中的所有程序集。首先标记顶级、根类型、方法、属性、字段等,例如,向场景中的游戏对象添加的 MonoBehaviour 派生类便是根类型。然后,UnityLinker 分析已标记为要进行识别的根,并标记这些根所依赖的托管代码。完成此静态分析后,所有剩余的未标记代码都无法通过应用程序代码中的任何执行路径来访问,并将从程序集中删除。
请注意,这一过程不会对代码进行混淆处理。
UnityLinker 不能总是检测出项目中的代码通过反射时来引用其他代码的实例,因此可能会误删除实际在使用的代码。将 Managed Stripping Level 设置从 Low 提升为 High 时,代码剥离导致游戏中发生意外行为变化的风险也会增加。这种行为变化小到细微的逻辑变化,大到调用缺失方法造成的崩溃。
UnityLinker 能够检测和处理一些反射模式。如需查看该工具可以处理的最新模式的示例,请参阅 Mono IL Linker 反射测试套件。但是,如果使用的不仅仅是简单的反射,必须给 UnityLinker 一些提示,说明哪些类不应该被处理。可以通过 link.xml 文件和 Preserve 属性的形式提供这些提示:
UnityLinker 在分析程序集中未使用的代码时,会将使用属性或 link.xml 文件保留的每个元素视为根元素。
在源代码中使用 [Preserve] 属性可防止 UnityLinker 剥离该代码。下面的列表描述了在对不同的代码元素应用 Preserve 属性时 UnityLinker 会保留哪些实体:
__Assembly__:保留程序集内的所有类型(就好像您为每个类型输入了 [Preserve] 属性一样)。要为程序集分配 Preserve 属性,请将该属性声明放在程序集包含的任何 C# 文件中,但需在所有命名空间声明之外:
using System;
using UnityEngine.Scripting;
[assembly: Preserve]
namespace Example
{
public class Foo {}
}
__Type__:保留类型及其默认构造函数。
Method: Preserves the method, its declaring type, return type, and the types of all of its arguments.
Property: Preserves the property, its declaring type, value type, the getter method, and the setter method.
Field: Preserves the field, its declaring type, and the field type.
Event: Preserves the event, its declaring type, return type, the add method, and the remove method.
Delegate: Preserves the delegate type and all of its methods.
请注意,相比使用 Preserve 属性,在 link.xml 文件中标记代码实体可以提供更强的控制。例如,用 Preserve 属性修饰某个类既会保留类型,也会保留默认构造函数。而使用 link.xml 文件,可以选择只保留类型(不保留默认构造函数)。
可以在任何程序集中和任何命名空间中定义 Preserve 属性。因此,可以使用 UnityEngine.Scripting.PreserveAttribute 类、为其创建子类或创建您自己的 PreserveAttribute 类,例如:
class Foo
{
[UnityEngine.Scripting.Preserve]
public void UsingUnityPreserve(){}
[CustomPreserve]
public void UsingCustomPreserve(){}
[Preserve]
public void UsingOwnPreserve(){}
}
class CustomPreserveAttribute : UnityEngine.Scripting.PreserveAttribute {}
class PreserveAttribute : System.Attribute {}
使用 [assembly: UnityEngine.Scripting.AlwaysLinkAssembly] 属性可强制 UnityLinker 处理程序集(无论程序集是否被构建中包含的另一个程序集引用)。AlwaysLinkAssembly 属性只能在程序集上定义。
此属性仅指示 UnityLinker 将其__根标记规则 (Root Marking Rules)__ 应用于程序集。该属性本身不会直接使程序集中的代码被保留。如果没有代码元素与程序集的根标记规则匹配,UnityLinker 仍会从构建中删除程序集。
可在包含一个或多个方法的包或预编译程序集上将此属性与 [RuntimeInitializeOnLoadMethod] 属性结合使用,但后者可能不包含在项目的任何场景中直接或间接使用的类型。
如果程序集定义 [assembly: AlwaysLinkAssembly] 属性并由构建中包含的其他程序集所引用,则该属性对于输出没有任何影响。
link.xml 文件是一个基于项目的列表,其中声明如何保留程序集以及程序集中的类型和其他代码实体。要使用 link.xml 文件,请创建此文件(请参阅下面的示例),并将其放入项目的 Assets 文件夹(或者 Assets 文件夹的任何子目录)。可以在项目中使用任意数量的 link.xml 文件,因此插件可以提供自己的保留声明。UnityLinker 会将 link.xml 文件中保留的任何程序集、类型或成员都视为根类型。
请注意,在软件包中不支持 link.xml 文件,但可以从非软件包 link.xml 文件中引用软件包程序集。
以下示例说明了使用 link.xml 文件声明项目程序集的根类型时可以采用的不同方式:
<linker>
<!--
Preserve types and members in an assembly
-->
<assembly fullname="Assembly1">
<!--Preserve an entire type-->
<type fullname="Assembly1.A" preserve="all"/>
<!--No "preserve" attribute and no members specified
means preserve all members-->
<type fullname="Assembly1.B"/>
<!--Preserve all fields on a type-->
<type fullname="Assembly1.C" preserve="fields"/>
<!--Preserve all fields on a type-->
<type fullname="Assembly1.D" preserve="methods"/>
<!--Preserve the type only-->
<type fullname="Assembly1.E" preserve="nothing"/>
<!--Preserving only specific members of a type-->
<type fullname="Assembly1.F">
<!--
Fields
-->
<field signature="System.Int32 field1" />
<!--Preserve a field by name rather than signature-->
<field name="field2" />
<!--
Methods
-->
<method signature="System.Void Method1()" />
<!--Preserve a method with parameters-->
<method signature="System.Void Method2(System.Int32,System.String)" />
<!--Preserve a method by name rather than signature-->
<method name="Method3" />
<!--
Properties
-->
<!--Preserve a property, its backing field (if present),
getter, and setter methods-->
<property signature="System.Int32 Property1" />
<property signature="System.Int32 Property2" accessors="all" />
<!--Preserve a property, its backing field (if present), and getter method-->
<property signature="System.Int32 Property3" accessors="get" />
<!--Preserve a property, its backing field (if present), and setter method-->
<property signature="System.Int32 Property4" accessors="set" />
<!--Preserve a property by name rather than signature-->
<property name="Property5" />
<!--
Events
-->
<!--Preserve an event, its backing field (if present),
add, and remove methods-->
<event signature="System.EventHandler Event1" />
<!--Preserve an event by name rather than signature-->
<event name="Event2" />
</type>
<!--Examples with generics-->
<type fullname="Assembly1.G`1">
<!--Preserve a field with generics in the signature-->
<field signature="System.Collections.Generic.List`1<System.Int32> field1" />
<field signature="System.Collections.Generic.List`1<T> field2" />
<!--Preserve a method with generics in the signature-->
<method signature="System.Void Method1(System.Collections.Generic.List`1<System.Int32>)" />
<!--Preserve an event with generics in the signature-->
<event signature="System.EventHandler`1<System.EventArgs> Event1" />
</type>
<!--Preserve a nested type-->
<type fullname="Assembly1.H/Nested" preserve="all"/>
<!--Preserve all fields of a type if the type is used. If the type is not
used it will be removed-->
<type fullname="Assembly1.I" preserve="fields" required="0"/>
<!--Preserve all methods of a type if the type is used.
If the type is not used it will be removed-->
<type fullname="Assembly1.J" preserve="methods" required="0"/>
<!--Preserve all types in a namespace-->
<type fullname="Assembly1.SomeNamespace*" />
<!--Preserve all types with a common prefix in their name-->
<type fullname="Prefix*" />
</assembly>
<!--Preserve an entire assembly-->
<assembly fullname="Assembly2" preserve="all"/>
<!--No "preserve" attribute and no types specified means preserve all-->
<assembly fullname="Assembly3"/>
<!--Fully qualified assembly name-->
<assembly fullname="Assembly4, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
<type fullname="Assembly4.Foo" preserve="all"/>
</assembly>
<!--Force an assembly to be processed for roots but don’t explicitly preserve
anything in particular. Useful when the assembly is not referenced.-->
<assembly fullname="Assembly5" preserve="nothing"/>
</linker>
link.xml 文件的 <assembly> 元素有三个特殊用途的属性:
ignoreIfMissing 如果需要为在所有播放器构建过程中不存在的程序集声明保留,请在 link.xml 文件中的 <assembly> 元素上使用 ignoreIfMissing 属性:
<linker>
<assembly fullname="Foo" ignoreIfMissing="1">
<type name="Type1"/>
</assembly>
</linker>
ignoreIfUnreferenced
在某些情况下,您可能希望仅当程序集被其他程序集引用时才保留程序集中的实体。可以在 link.xml 文件中的 <assembly> 元素上使用 ignoreIfUnreferenced 属性,这样仅当程序集中引用至少一种类型时才保留程序集中的实体。
<linker>
<assembly fullname="Bar" ignoreIfUnreferenced="1">
<type name="Type2"/>
</assembly>
</linker>
__注意:__进行引用的程序集中的代码本身是否被剥离并不重要,被引用的程序集中具有此属性的指定元素仍会被保留。
windowsruntime
为 Windows 运行时元数据 (.winmd) 程序集定义保留时,必须在 link.xml 文件中的 <assembly> 元素中添加 windowsruntime=“true” 属性:
<linker>
<assembly fullname="Windows" windowsruntime="true">
<type name="Type3"/>
</assembly>
</linker>
Unity Editor 会将包含 Unity 项目任何场景使用的类型的程序集列表进行合并,并将其传递给 UnityLinker。然后,UnityLinker 处理这些程序集、这些程序集的任何引用、link.xml 文件中声明的任何程序集以及具有 AlwaysLinkAssembly 属性的任何程序集。通常,项目中不属于这些类别的任何程序集都不会被 UnityLinker 处理,也不会包含在播放器构建中。
UnityLinker 在处理每个程序集时遵循一组规则,这组规则基于程序集分类,程序集是否包含场景中使用的类型,以及为构建选择的 Managed Stripping Level 属性值。
根据这些规则的用途,程序集划为以下几种分类:
.NET 类库程序集 — 包括 Mono 类库(如 mscorlib.dll 和 System.dll)以及 .NET 类库外观程序集(如 netstandard.dll)。
平台 SDK 程序集 — 包括特定于平台 SDK 的托管程序集。例如,通用 Windows 平台 SDK 中包含的 windows.winmd 程序集。
Unity 引擎模块程序集 — 包括构成 Unity 引擎的托管程序集,如 UnityEngine.Core.dll。
项目程序集 — 包括特定于项目的程序集,例如:
脚本程序集,如 Assembly-CSharp.dll
预编译的程序集
软件包程序集
以下各节将详细介绍 UnityLinker 如何针对每种 Managed Stripping Level 设置进行标记以及保留或剥离程序集代码:
将 Managed Stripping Level 设置为 Low 时,UnityLinker 将根据一组保守的规则删除代码,因此应该会删除大多数无法访问的代码,同时尽量降低实际使用的代码被剥离的可能性。__低__剥离级别优先考虑可用性,其次才是减小代码大小。
根标记规则决定了 UnityLinker 如何识别程序集中的顶级类型。
程序集类型 | 操作 | 根标记规则 |
---|---|---|
.NET 类和平台 SDK | 剥离 | 应用预防性保留 |
任何 link.xml 中定义的保留 | ||
包含场景中所用类型的程序集 | 复制 | 标记程序集中的所有类型和成员 |
其他全部 | 剥离 | 标记所有公共类型 |
标记公共类型的所有公共成员 | ||
标记包含 [RuntimeInitializeOnLoadMethod] 属性的方法 | ||
标记包含 [Preserve] 属性的类型和成员 | ||
任何 link.xml 中定义的保留 | ||
在下列程序集中标记派生自 MonoBehaviour 和 ScriptableObject 的所有类型: 预编译程序集 软件包程序集 程序集定义程序集 Unity 脚本程序集 |
||
测试 | 剥离 | 标记包含 NUnit.Framework 中定义的任何属性的方法。例如:[Test] |
标记包含 [UnityTest] 属性的方法 |
注意:____剥离__操作意味着 UnityLinker 会分析程序集来找出可以删除的代码。复制__操作意味着 UnityLinker 可将整个程序集复制到最终构建(并且将其中的所有类型都标记为根类型)。
标记根类型后,UnityLinker 将执行静态分析以识别这些根所依赖的所有代码。
规则目标 | 描述 |
---|---|
Unity 类型 | 当 UnityLinker 标记派生自 MonoBehaviour 的类型时,还会标记该类型的所有成员。 |
当 UnityLinker 标记派生自 ScriptableObject 的类型时,还会标记该类型的所有成员。 | |
特性 | UnityLinker 在所有标记的程序集、类型、方法、字段、属性等对象中标记相应属性。 |
调试属性 | 启用脚本调试时,UnityLinker 将标记具有 [DebuggerDisplay] 属性的所有成员(即使没有代码路径使用该成员)。 |
.NET 外观类库程序集 | 外观程序集是指 .NET 类库中负责将类型定义转发给另一个程序集的程序集。例如,netstandard.dll (属于 .NET Standard 2.0 API 兼容性级别)就是一个外观程序集,它定义 .NET 接口,但将该接口的实现转发给其他 .NET 程序集。外观程序集在运行时并不是严格必要的,但是,由于可以编写依赖这些程序集的反射代码,因此低剥离级别会保留这些程序集。 |
在下面的示例中,假设在代码中的任何地方都没有使用 Foo.UnusedProperty
属性。通常,UnityLinker 会剥离该属性,但当您启用脚本调试时,它将标记 Foo.UnusedProperty
并保留该属性,因为 [DebuggerDisplay] 属性在 Foo
中。
[DebuggerDisplay("{UnusedProperty}")]
class Foo
{
public int UnusedProperty { get; set; }
}
将 Managed Stripping Level 设置为 Medium 时,UnityLinker 将根据一组规则删除代码,确保在__低 (Low)__ 剥离级别和__高 (High)__ 剥离级别之间取得平衡。中等 (Medium) 剥离级别不像__低__剥离级别那么谨慎,也不如__高__剥离级别那么极端。因此,因删除代码而导致意外副作用的风险大于__低__剥离级别,但小于__高__剥离级别。
程序集类型 | 操作 | 根标记规则 |
---|---|---|
.NET 类和平台 SDK | 剥离 | 与低剥离级别相同,但:不适用预防性保留 |
包含场景中引用的类型的程序集 | 剥离 | 不自动标记程序集中的所有类型和成员 |
标记包含 [RuntimeInitializeOnLoadMethod] 属性的方法 | ||
标记包含 [Preserve] 属性的类型和成员 | ||
任何 link.xml 中定义的保留 | ||
在下列程序集中标记派生自 MonoBehaviour 和 ScriptableObject 的所有类型: 预编译程序集 软件包程序集 程序集定义程序集 Unity 脚本程序集 |
||
其他全部 | 剥离 | 与低剥离级别相同,但: 不自动标记公共类型 不自动标记公共类型的公共成员 |
测试 | 剥离 | 与低剥离级别相同 |
规则目标 | 描述 |
---|---|
Unity 类型 | 与低剥离级别相同 |
特性 | 与低剥离级别相同 |
调试属性 | 与低剥离级别相同 |
.NET 外观类库程序集 | 与低剥离级别相同 |
将 Managed Stripping Level 设置为 High 时,UnityLinker 将尽可能移除无法访问的代码,并生成比__中等 (Medium)__ 剥离级别更小的构建。高 (High) 剥离级别优先考虑减小代码大小,而不是确保可用性;您可能需要添加 link.xml 文件、Preserve 甚至重写有问题的代码部分。
程序集类型 | 操作 | 根标记规则 |
---|---|---|
.NET 类和平台 SDK | 剥离 | 与中等剥离级别相同 |
包含场景中引用的类型的程序集 | 剥离 | 与中等剥离级别相同 |
其他全部 | 剥离 | 与中等剥离级别相同 |
测试 | 剥离 | 与低剥离级别相同 |
Link.xml 文件支持不常使用的 “features” XML 属性。例如,mscorlib.dll 中嵌入的 mscorlib.xml 文件使用此属性,但在适当情况下,您可以在任何 link.xml 文件中使用该属性。
在__高__级别剥离期间,UnityLinker 根据当前构建的设置来排除不支持的功能的保留:
例如,下面的 link.xml 文件在支持 COM 的平台上保留某种类型的一个方法,并在所有平台上保留一个方法:
<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>
规则目标 | 描述 |
---|---|
Unity 类型 | 与低剥离级别相同 |
特性 | 在所有标记的程序集、类型和成员上,如果属性类型也已经被标记,那么 UnityLinker 将标记属性。 注意,UnityLinker 总是保留某些属性,因为运行时需要这些属性。 UnityLinker 从所有程序集、类型和成员中删除与安全性相关的属性(如 System.Security.Permissions.SecurityPermissionAttribute)。 |
调试属性 | UnityLinker 会始终删除调试属性,如 DebuggerDisplayAttribute 和 DebuggerTypeProxyAttribute。 |
.NET 外观类库程序集 | 与保留所有 .NET 外观程序集的中低剥离级别不同,高剥离级别将删除所有外观,因为在运行时不需要它们。 在剥离后,以外观程序集存在为前提的反射代码将不起作用。 |
设置__高__剥离级别时,UnityLinker 将编辑方法体,以进一步减小代码大小。本节总结了 UnityLinker 对方法体所做的一些值得注意的编辑。
UnityLinker 目前只编辑 .NET 类库程序集中的方法体。注意,在方法体编辑之后,程序集的源代码不再与程序集中经过编译的代码匹配,因此可能使调试变得更加困难。
UnityLinker 会删除用于检查 System.Environment.OSVersion.Platform
并且当前目标平台无法访问的 If 语句块。
对于获取或设置字段的方法调用,UnityLinker 会替换为直接字段访问。这种做法通常能够完全剥离方法,有助于降低大小。
如果目标是 Mono 后端,那么仅当方法的调用者获准直接访问字段(根据字段的可见性),UnityLinker 才会进行此更改。对于 IL2CPP,可见性规则不适用,因此 UnityLinker 会在适当情况下均进行此更改。
UnityLinker 会对只返回常量值的方法调用进行内联。
UnityLinker 会删除内容为空以及返回类型为 void
的方法调用。
UnityLinker 会在 Finally 块为空时删除 Try/Finally 模块。删除空调用可能会产生空的 Finally 块。在方法编辑过程中发生这种情况时,UnityLinker 将删除整个 Try/Finally 块。可能发生这种情况的一个情形是编译器在 foreach 循环中生成 Try/Finally 块来调用 Dispose()
。