本文内容
本文介绍:
- 构建时代码裁剪
- 裁剪存在的问题
- Preserve特性(
UnityEngine.Scripting.PreserveAttribute
)
自动代码裁剪(Managed code stripping)
在Edit->Project Settings->Player->(特定平台)Optimization->Managed Stripping Level中可以选择构建时代码裁剪的程度。
代码裁剪将在构建过程中删除未使用或不可访问的代码,以显著减少程序的最终大小。
该过程会尝试从:你脚本所生成的程序集、插件或包的程序集、.Net Framework程序集中裁剪不可达代码。
裁剪的问题
- 一般情况:代码引用都可在编译时确定,不会存在任何问题。
- 反射调用:但反射(
Reflection
)调用(借助字符串等动态读取数据生成对象或调用方法)无法在编译时追踪。
因此在提升裁剪程度时,被反射调用的代码可能会在裁剪过程中被移除。当尝试调用到这些不存在代码时,会出现各种各样令人意外的错误。
比如我所遇到的问题:
我使用某个JSON库进行序列化与反序列化保存游戏,当在Unity编辑器中,一切正常;当裁剪等级设置为High时,构建后加载存档产生如下错误:
可以归纳出:
- 这是一个反射相关的错误
- 涉及
Linq.Where()
函数的调用
当存在以下要素:
- 构建后运行存在反射错误
- 编辑器中运行一切正常
- 使用了构建裁剪
基本上可以断定是代码裁剪把某些重要代码给意外裁剪掉了。一般在Print出错误堆栈时,你可以在里面找到一些端倪(比如调用过程涉及的函数名或类名)。但我这个错误实属有点阴间,它的错误堆栈一共就只有两行。
但好歹给了些提示,涉及LInq.Where()
。仔细思考,JSON序列化库应该不会使用到Linq
(经过代码查找也确实如此),因此我把重心放在了我的脚本中。最后发现:
在我存档的序列化对象中,存在一处Linq.Where()
的调用。
但是下面的构造函数LevelData(LevelManager levelInfo)
是用于保存的;上面的无参构造函数LevelData()
才是用于加载的。
为什么在加载时,反射生成存档对象时没有匹配上面的无参构造函数,而是匹配了下面的有参函数?原因就是……无参构造函数压根就不存在于构建后的代码中。
可以看到,LevelData()
的引用个数是0,结合裁剪等级设置为High,基本可以断定,这个函数将在构建时移除。
Preserve特性
[Preserve]
可以显式声明在构建中保留的代码。可作用于:类、方法、字段或属性。
以下是使用该特性解决上述问题的例子:
using UnityEngine.Scripting;
public class LevelData
{
......
[Preserve]
public LevelData() //构造函数将一定保留
{
}
}
受这个特性修饰的所有单位将在裁剪时无条件保留。我的问题也在相关代码加入了[Preserve]
修饰后解决。
总结
吾日三省吾身:
- 有使用代码裁剪乎?
- 代码存在反射调用乎?
- Preserve乎?