.NET NativeAOT 指南

💡 原文中文,约7000字,阅读约需17分钟。
📝

内容提要

随着.NET 8的发布,NativeAOT应用模型开始广泛应用,提高应用性能并节省资源。使用NativeAOT需注意处理限制,如反射和动态泛型实例化。解决方案包括使用DynamicDependency和DynamicallyAccessedMembers属性,以及TrimmerRootAssembly和Runtime Directives文件。

🔎

延伸解读

NativeAOT 的编译机制与依赖图

NativeAOT 并非简单剪裁未使用代码,而是通过扫描 IL 代码构建依赖图,仅编译图中引用的内容。依赖图在编译期间静态构建,任何无法静态分析的代码都不会被编译。这解释了为何反射和动态泛型实例化容易导致运行时失败,因为编译器无法预知这些动态行为。

反射与动态泛型实例化的典型陷阱

反射调用若目标无法从字面量推断,编译器不会生成对应代码,导致运行时异常。动态泛型实例化如 Point<int> 也无法自动推断,即使使用 TrimmerRootAssembly 将整个程序集作为根,泛型实例化仍需单独构造。因此,依赖动态特性的代码需显式告知编译器。

解决依赖问题的推荐做法

优先使用 DynamicallyAccessedMembers 和 DynamicDependency 属性标注代码,明确告知编译器需要保留的类型成员或依赖关系。对于泛型实例化,可通过 Runtime Directives 文件指定,但此方法不推荐。TrimmerRootAssembly 和 TrimmerRootDescriptor 适用于不拥有代码或需精细控制的情况,但无法解决泛型实例化问题。

NativeAOT 的适用场景与收益

NativeAOT 能提升应用性能,降低内存占用和二进制大小,并支持不允许 JIT 编译的平台,如 iOS 和主机平台,甚至嵌入式设备。然而,其限制要求开发者提前处理反射和泛型实例化问题,否则可能导致运行时错误。理解这些机制有助于更高效地利用 NativeAOT。

❓

Q&A

如何在 .NET 8 中启用 NativeAOT 发布应用?

在发布应用时使用 MSBuild 传递属性 PublishAot=true,例如:dotnet publish -c Release -r win-x64 /p:PublishAot=true。其中 win-x64 是运行时标识符,可以替换为 linux-x64、osx-arm64 或其他平台。发布的应用可以在 bin/Release/<target framework>/<runtime identifier>/publish 中找到。

NativeAOT 编译过程分为哪几个阶段?

NativeAOT 编译包括两个阶段:首先扫描 IL 代码,构建整个程序视图(一个依赖图),其中包含所有需要编译的必要依赖节点;然后对依赖图中的每个方法进行实际的编译,生成代码。在编译过程中可能会出现一些“延迟”的依赖,因此这两个阶段可能会交错出现。

为什么使用反射时 NativeAOT 会失败?如何解决?

因为 NativeAOT 的依赖图是在编译期间静态构建的,反射无法被静态分析,所以编译器不会为反射使用的类型生成代码,导致运行时异常。解决方案包括:使用 DynamicDependency 属性告知编译器依赖关系,或使用 DynamicallyAccessedMembers 属性让编译器为动态访问的成员生成代码。

NativeAOT 下动态泛型实例化有什么问题?如何解决?

动态泛型实例化在 NativeAOT 下无法被编译器推断,因此不会生成特定实例化的代码,导致运行时失败。解决方案包括:在代码中提前使用这些实例化(例如通过不可达的条件分支),或使用 Runtime Directives 文件(rd.xml)指定需要编译的泛型实例化。

TrimmerRootAssembly 和 TrimmerRootDescriptor 在 NativeAOT 中有什么作用?

TrimmerRootAssembly 用于将指定程序集中的所有类型和方法都作为根,确保它们被编译。TrimmerRootDescriptor 允许高级用户通过 link.xml 文件控制从程序集中包含哪些内容。但注意,这两种方法都不适用于泛型实例化。

NativeAOT 有哪些优势?

NativeAOT 可以提高应用性能,节省资源(更低的内存占用和更小的二进制大小),支持不允许 JIT 编译的平台(如 iOS 和主机平台),并使 .NET 能够运行在嵌入式设备甚至裸机设备上(例如在 UEFI 上运行)。

🏷️

标签

➡️

继续阅读