实用技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > ASP.NET > 实用技巧 > .NET AOT

.NET AOT 详解

作者:我是唐青枫

AOT是一种将代码直接编译为机器码的技术,与传统的 JIT(Just-In-Time Compilation)编译方式形成对比,本文给大家介绍.NET AOT的相关知识,感兴趣的朋友一起看看吧

简介

AOT(Ahead-Of-Time Compilation)是一种将代码直接编译为机器码的技术,与传统的 JIT(Just-In-Time Compilation)编译方式形成对比。在.NET 中,AOT 编译可以在应用发布时将 IL(中间语言)代码转换为平台特定的机器码,而不是在运行时进行 JIT 编译。

与 JIT 的区别

JIT(即时编译)

AOT(预编译)

.NET 中主要的 AOT 形式

ReadyToRun(R2R)

使用示例(.NET 6/7 均适用)

<PropertyGroup>
  <PublishReadyToRun>true</PublishReadyToRun>
  <!-- 可以指定针对某一架构:anycpu、x64、arm64 等 -->
  <PublishReadyToRunUseCrossgen2>true</PublishReadyToRunUseCrossgen2>
</PropertyGroup>

运行

dotnet publish -c Release -r win-x64

优缺点:

Native AOT(以前称为 CoreRT、.NET Native)

使用示例

<PropertyGroup>
  <!-- 标记为可 AOT 发布 -->
  <PublishAot>true</PublishAot>
  <!-- 发布时去除不需要的依赖,减小体积 -->
  <PublishTrimmed>true</PublishTrimmed>
  <!-- 指定运行时环境,比如 win-x64, linux-x64, linux-arm64 等 -->
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  <!-- 若项目使用 WinForms/WPF,则需要设置此项为 false 或调试 -->
  <SelfContained>true</SelfContained>
</PropertyGroup>

运行

dotnet publish -c Release

完成后,会在 bin\Release\net7.0\win-x64\publish\ 目录下得到一个 .exe(或对应平台无后缀可执行文件)。

优缺点:

Mono AOT(Xamarin / Unity 场景)

使用示例:

优缺点:

AOT 在 .NET 中的演进与对比

特性 / 版本.NET Core 3.x & .NET 5/6 R2R.NET 7/8 Native AOTMono AOT(Xamarin/Unity)
最终产物包含 IL + 已编译部分类别的 .dll/.exe纯本机可执行文件(或较少依赖文件)本机代码 + Mono 运行时库
运行时依赖依赖 CoreCLR零依赖或极少依赖(视 SelfContained 而定)依赖 Mono 运行时
启动速度明显优于纯 JIT,但仍有部分 JIT最优;几乎无需运行期编译开销较优于 JIT,但受限于 Mono 负载
支持的应用类型全部(包括 ASP.NET Core)控制台、工具类应用;对 ASP.NET Core 支持有限移动端(iOS/Android)、Unity 游戏
反射 / 动态代码全量支持;运行时仍可 JIT需手动指定反射保留;不支持动态生成 IL大部分反射可用,但动态 IL 支持受限
发布包体积较 IL + JIT 稍大可经修剪后显著减小;自行选择不同运行时通常最大,因为包含 Mono 运行时和 AOT 文件

为什么要使用 AOT

加快冷启动

减少运行时依赖

提高性能可预测性

安全性 / 平台限制

二进制可移植性

扫描可达程序集

生成本机代码

处理反射 / 动态需求

.NET 7+DynamicDependencyAttribute、PreserveDependency 等方式告知编译器保留反射访问所需类型。

生产最终可执行文件

如何在项目中启用 AOT

ReadyToRun(R2R)示例

.csproj 中添加属性:

<PropertyGroup>
  <!-- 启用 ReadyToRun 预编译 -->
  <PublishReadyToRun>true</PublishReadyToRun>
  <!-- 使用 CrossGen2 进行预编译(建议在 .NET 6+ 使用) -->
  <PublishReadyToRunUseCrossgen2>true</PublishReadyToRunUseCrossgen2>
  <!-- 可选:指定是否在发布时生成非托管符号文件 -->
  <PublishReadyToRunEmitSymbols>true</PublishReadyToRunEmitSymbols>
  <!-- 指定具体目标平台 -->
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>

然后执行:

dotnet publish -c Release -r win-x64 --self-contained false

Native AOT 示例

.csproj 中添加:

<PropertyGroup>
  <!-- 开启 Native AOT 发布 -->
  <PublishAot>true</PublishAot>
  <!-- 开启修剪,减小体积 -->
  <PublishTrimmed>true</PublishTrimmed>
  <!-- 例如发布为控制台应用,可选改为 false 如果涉及 WinForms/WPF -->
  <SelfContained>true</SelfContained>
  <!-- 目标运行时标识符 -->
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  <!-- 可选:发布时同时生成调试符号 -->
  <DebugType>embedded</DebugType>
  <!-- 如需使用单文件发布 -->
  <PublishSingleFile>true</PublishSingleFile>
  <!-- 可选:剔除 Diagnostics 诊断支持以进一步减小体积 -->
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

然后运行:

dotnet publish -c Release

注意:

AOT 的优缺点及适用场景

优点

极快启动:

部署简单:

更小内存占用峰值:

可预测走向发布:

符合某些平台限制:

缺点

体积膨胀 vs 兼容性:

ReadyToRun 相对于纯 IL 包增量并不大,但 Native AOT 若不精心修剪,体积可能与完整运行时相当;

某些第三方库对 AOT 支持不好,可能需要额外适配。

有限的运行时代码生成:

无法做 Reflection.Emit、动态生成表达式树等,需要在编译期预先声明;

运行时使用 System.Text.Json、Newtonsoft.Json 等反射型序列化/反序列化时,需手动配置 JsonSerializerContext 或显式注册要序列化的类型。

调试和诊断不便:

Native AOTStackTrace 可能缺少符号映射;

如出现 CPU 性能或内存泄漏问题,无法借助 JIT 时代的动调。

不适合大型动态场景:

典型适用场景

命令行工具(CLI)

微服务/Serverless 函数

桌面/移动端轻量应用

IoT/嵌入式设备

创建项目

dotnet new console -n AotDemo
cd AotDemo

修改项目文件 AotDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <!-- 开启 Native AOT -->
    <PublishAot>true</PublishAot>
    <!-- 发布时进行修剪 -->
    <PublishTrimmed>true</PublishTrimmed>
    <!-- 将所有依赖打包到单个可执行文件里 -->
    <PublishSingleFile>true</PublishSingleFile>
    <!-- 自包含部署(包含运行时) -->
    <SelfContained>true</SelfContained>
    <!-- 运行时标识符,根据目标平台调整 -->
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <!-- 便于调试,可以嵌入 PDB 符号 -->
    <DebugType>embedded</DebugType>
  </PropertyGroup>
</Project>

编写简单代码 Program.cs

using System;
using System.Reflection;
namespace AotDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello Native AOT World!");
            // 示例:试图使用反射读取自身类型
            var type = typeof(Program);
            Console.WriteLine($"当前类型:{type.FullName}");
        }
    }
}

编译并发布

在项目根目录执行:

dotnet publish -c Release

发布完成后,打开 bin\Release\net7.0\win-x64\publish\,可以看到 AotDemo.exeAotDemo

运行与验证

直接双击或命令行执行 AotDemo.exe./AotDemo,输出:

Hello Native AOT World!
当前类型:AotDemo.Program

分析产物体积

如果使用了反射

诊断和调试

rd.xml 配置文件

作用

.NET Native AOT、ReadyToRun + 修剪(ILLinker)等场景中,编译器或 IL 链接器会在发布阶段对中间语言(IL)进行分析与“修剪”(Trim),剔除未被静态调用或引用的类型、成员与元数据,以减小输出体积、提升启动性能。然而,反射(Reflection)是一种运行时特性:代码中的某些类型、方法、属性等只有在运行阶段通过反射动态访问,编译时并不可见。若不做额外保留,ILLinker 在静态分析时会误判这些成员为“不可达”,进而被剔除,导致在运行时使用诸如 Activator.CreateInstance、Type.GetType、序列化/反序列化、ORM 映射等场景抛出 “找不到类型/成员” 的异常。

.rd.xml 文件是 .NET NativeILLink 场景下的“保留指令”描述文件(runtime directives XML)。通过在其中显式声明“要保留的程序集/类型/成员/属性”,可以避免它们在发布构建时被错误剔除,从而保证反射相关逻辑在运行时正常工作。

基本结构

一个典型的 .rd.xml(Runtime Directives XML)文件的根节点为 <Directives>,其下通常有一个 <Application><Library> 节点,后者根据项目类型(控制台应用、类库等)有所不同。

<?xml version="1.0" encoding="utf-8"?>
<Directives xmlns="http://schemas.microsoft.com/netcore/2013/01/metadata">
  <!-- 
    Application: 用于标记应用程序可达的根节点; 
    Library: 用于类库时的配置(一般也可放在 Application 节点中)。
  -->
  <Application>
    <!-- 在这里声明要保留的程序集、类型、成员等 -->
  </Application>
</Directives>

其中常用的子节点包括:

常用配置项说明

<Assembly><Type> 节点上,常见的属性及含义如下:

.rd.xml 示例演示

保留整个程序集(全部类型和成员)

假设项目中有一个 MyApp.Core.dll,并且在运行时会通过反射访问其中的所有类型(如插件、动态加载等)。若要保留 MyApp.Core 程序集中所有内容,可以这样写:

<?xml version="1.0" encoding="utf-8"?>
<Directives xmlns="http://schemas.microsoft.com/netcore/2013/01/metadata">
  <Application>
    <!-- 忽略版本号、Culture、PublicKeyToken,以宽松方式匹配 MyApp.Core -->
    <Assembly Name="MyApp.Core" Dynamic="Required All" Serialize="Required All" />
  </Application>
</Directives>

保留特定类型(及其成员)

若只希望针对某个类型(如 MyApp.Core.Services.PluginLoader)进行反射保留:

<Assembly Name="MyApp.Core">
  <Type Name="MyApp.Core.Models.Entity`1" Dynamic="Required All">
    <!-- 指定泛型实例化需求 -->
    <GenericInstantiation>
      <TypeName>MyApp.Core.Models.Entity`1[[MyApp.Core.Models.Customer, MyApp.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]</TypeName>
    </GenericInstantiation>
  </Type>
</Assembly>

若该类型是泛型类型,例如 MyApp.Core.Models.Entity<T>,且会在运行时实例化某个具体泛型(如 Entity<Customer>)进行反射构造,需要这样指定:

<Assembly Name="MyApp.Core">
  <Type Name="MyApp.Core.Models.Entity`1" Dynamic="Required All">
    <!-- 指定泛型实例化需求 -->
    <GenericInstantiation>
      <TypeName>MyApp.Core.Models.Entity`1[[MyApp.Core.Models.Customer, MyApp.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]</TypeName>
    </GenericInstantiation>
  </Type>
</Assembly>

保留 JSON(或其他序列化)所需成员

在使用 System.Text.Json 的源生成(source generator)方式时,可以通过 [JsonSerializable] 等特性生成上下文,通常无需 .rd.xml。但如果使用反射式序列化(如 Newtonsoft.Json 的默认行为),则需要保留类型的公共 getter/setter 属性

namespace MyApp.Core.Models
{
    public class Customer
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        private DateTime Birthday { get; set; }
        public string SecretCode { get; private set; }
    }
}

若要保证 JSON 序列化能访问到 Id、Name、SecretCode,可以这样配置:

<Directives xmlns="http://schemas.microsoft.com/netcore/2013/01/metadata">
  <Application>
    <Assembly Name="MyApp.Core">
      <!-- 
        保留 Customer 类型的公有属性和字段 
        Serialize="Required Public" 表示仅保留 public 字段/属性 
      -->
      <Type Name="MyApp.Core.Models.Customer" Dynamic="Required Public" Serialize="Required Public" />
    </Assembly>
  </Application>
</Directives>

这里 Dynamic="Required Public" 也会保留公有方法,若无需公有方法也可只用 Serialize="Required Public"。如果只想保留序列化场景的 public 属性,把 Dynamic 去掉或设为更窄范围也可行。

保留特定成员(Method / Field / Property)

有时只需保留某个类型下的某个方法或字段,而不是整类型

<Directives xmlns="http://schemas.microsoft.com/netcore/2013/01/metadata">
  <Application>
    <Assembly Name="MyApp.Core">
      <Type Name="MyApp.Core.Utils.Helper">
        <!-- 仅保留名为 DoWork 的公有实例方法 -->
        <Method Name="DoWork" Dynamic="Required Public" />
        <!-- 仅保留名为 _secretKey 的私有字段 -->
        <Field Name="_secretKey" Dynamic="Required All" />
        <!-- 仅保留 Id 属性的 Getter / Setter -->
        <Property Name="Id" Dynamic="Required Public" />
      </Type>
    </Assembly>
  </Application>
</Directives>

在项目中集成 .rd.xml

.rd.xml 文件放到项目根目录并在 .csproj 中进行声明,确保编译时能被识别并生效。

在项目根目录(与 .csproj 同级)放置文件,命名为 rd.xml,在 .csproj 中引用 .rd.xml

<PropertyGroup>
  <!-- 启用 Native AOT 发布 -->
  <PublishAot>true</PublishAot>
  <!-- 启用修剪 -->
  <PublishTrimmed>true</PublishTrimmed>
  <!-- 标记要使用 rd.xml 作为保留指令 -->
  <TrimmerDefaultAction>link</TrimmerDefaultAction>
  <TrimmerRootDescriptorFiles>rd.xml</TrimmerRootDescriptorFiles>
  <!-- 其他 AOT 相关配置 -->
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  <PublishSingleFile>true</PublishSingleFile>
  <SelfContained>true</SelfContained>
</PropertyGroup>

编译与发布命令

dotnet publish -c Release

编译器会自动读取 rd.xml,根据其中配置的保留指令对代码进行修剪,确保运行时反射需求的类型与成员被正确保留。

调试与验证

.csproj 中添加或在命令行指定:

<PropertyGroup>
  <!-- 打印链接器日志到指定文件 -->
  <TrimmerLogFile>trim-log.xml</TrimmerLogFile>
  <!-- 显示链接器分析的详细级别(可选:Skip、Silent、Verbose、diagnostic) -->
  <TrimmerLogLevel>diagnostic</TrimmerLogLevel>
</PropertyGroup>

发布后会在输出目录生成 trim-log.xml,打开后查找是否有某个类型/成员被修剪或被保留的记录。诊断级别输出会非常详细,便于查找“保留”是否生效,或哪些类型因遗漏而被剔除。

到此这篇关于.NET AOT 详解的文章就介绍到这了,更多相关.NET AOT内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文