搜索 Unity

关于优化的二三事:Unity 2020 LTS中的托管代码剥离

2021年10月25日 类别 技术 | 10 分 阅读
Population one
Population one
涵盖的主题
分享

托管代码剥离是构建过程中的一个关键步骤。引擎会在这时删去多余的代码,从而有效减少应用二进制文件的大小。 

删除后的代码不会被编译或出现在最终程序中,而以IL2CPP后端运行的项目也能有较低的内存占用。但如果托管代码剥离的精简程度太高,程序的运行时很有可能出现类型和方法的丢失(以及其他问题)。

在构建过程中,部分代码会被视作多余并剥离出来。这时,最简单的应对方法可以是手动将需要的程序集添加到link.xml文件中。在担任Unity软件开发顾问期间,我开展过几次项目审查,也时常从客户那听到关于如何更好地处理托管代码剥离的问题。所以我在这里收集了一些提示和最佳实践,介绍怎样用托管代码剥离的新注释特性(Annotation Attributes)来改善工作流。

Unity Linker的托管代码剥离

在使用IL2CPP脚本后端时,删除未使用的代码尤为重要。基于Mono IL Linker打造、为Unity定制的Unity Linker可通过静态分析来剥离托管中的代码。

Unity支持低、中、高三级IL2CPP托管代码剥离。代码剥离的过程、区分代码的因素以及三种剥离级别的区别可以在这本托管代码剥离手册了解。简单地说,代码剥离级别越高,Linker识别未使用代码的规则便越严格。而剥离等级可以在Player Settings的Managed Stripping Level中修改。

Linker会使用静态分析用来识别未使用代码,但该方法不能涵盖所有的情况,比如某个对象的类型只会在运行时被定义,这就会导致代码被错误地识别。尽管某些类或方法在编译时并没有被引用,但在运行时这些类或方法仍然是被需要的。一个很好的例子是使用了反射(Reflection)的代码。请仔细看看以下片段:

这段代码非常常见,但Linker并不知道MyAssembly、MyType和MyMethod在运行时是否真的被使用,因此它可能剥离这些代码,进而导致运行时报错。你可以在Unity手册的Scripting Restrictions(编程限制) 部分了解更多信息。

如果开发者使用的是Zenject等Dependency Injection(依赖输入)框架或Newtonsoft.Json等序列化库,则必须注意并针对性地解决错误的代码剥离。下方有一些常见的方法:

  • 让Linker识别所有特性,将无法识别的依赖留给我们用注释来解释。我们可以为需要保留的程序集、类和方法加上[Preserve]特性
  • 每个项目都可以有一个link.xml文件来说明那些程序集、类型和其他代码需要保存。你可以手动将需要的程序集、类和方法添加到link.xml,或使用UnityEditor API GenerateAdditionalLinkXmlFile在构建时生成link.xml文件。

Addressables软件包也使用有一个LinkXmlGenerator。Addressables的构建脚本会审查资源组的资源清单,并将用到的类型添加到link.xml文件中。它还会加入Addressables在运行时通过Reflection调用的类型。如果你想使用类似Scriptable Build Pipeline的构建方案,可以在引擎默认的BuildScriptPackedMode.cs构建脚本 中了解更多。

Unity支持同时使用多个保存于Assets文件夹或其子文件夹内的link.xml文件,所有文件都会在构建过程中被合并以供Linker参考。

Unity 2020 LTS托管代码剥离更新

[Preserve]特性需要由我们手动添加。但Unity 2020 LTS支持几种新的注释特性,可让你轻松准确地标记出应该保留的程序集、类和方法。这些特性包括:

  • RequireAttributeUsagesAttribute:当某特性类型被加上该特性时,该类型所有的CustomAttributes也将被标记,简化高级别剥离时的注释工作。
  • RequireDerivedAttribute:当某类型被加上该特性时,所有衍生类型也将同样被标记。
  • RequiredInterfaceAttribute:当某类型被加上该特性时,该类型下所有应用了的接口也都将被标记。
  • RequiredMemberAttribute:当某类型被加上该特性时,类型下所有带有[RequiredMember]的成员都会被标记。这些特性能让代码剥离更加准确,注释后的类型将可以被保留。然而,如果类本身未被使用,带有[RequiredMember]特性的组成类型也依旧会被剥离。
  • RequiredImplementorsAttribute:当某接口类型被加上该特性时,所有应用了该接口的类型也会被标记,省去为每处接口应用位置添加特性的必要。然而,如果该接口未应用于任意位置,即使带有[RequireImplementors]特性也仍会被删除。

在Unity 2020.1与2020.2中,Unity Linker的API经过更新与Mono IL Linker基本一致。工具现在可以检测一些简单的反射模式,因此升级到Unity 2020 LTS的用户更不需要link.xml文件了。

更多关于如何使用Unity 2020 LTS优化编程工作流的信息,请在这篇功能概述和Unity 2020 LTS手册的更新说明中了解。

展望未来

为测试人员和玩家提供高质量的应用包是我们2021年的目标之一,而改进代码剥离工作流也是实现该目标的重要一步。我们在2021.2版本中新增了一个新的“Minimal”代码剥离级别,并将其设为了IL2CPP的默认设置。请持续关注新的代码剥离选项。

Managed Stripping Level
请持续关注新的托管代码剥离级别。
2021年10月25日 类别 技术 | 10 分 阅读
涵盖的主题