简介
AOT(Ahead-Of-Time Compilation)
是一种将代码直接编译为机器码的技术,与传统的 JIT(Just-In-Time Compilation)
编译方式形成对比。在.NET
中,AOT
编译可以在应用发布时将 IL
(中间语言)代码转换为平台特定的机器码,而不是在运行时进行 JIT
编译。
与 JIT 的区别
JIT
(即时编译)
- 优点:灵活,运行时可以根据实际硬件做优化;对于不常用代码部分可延迟生成机器码,节省空间。
- 缺点:应用启动时会有
JIT
开销,若大量方法首次调用时需要编译,会影响“首次执行性能”(cold start
);运行时动态编译也会增加CPU
占用。
AOT
(预编译)
- 优点:发布包中已包含机器码,运行时不再需要编译,启动速度更快;减少运行时依赖(部分场景下可做到无
.NET
运行时依赖)。 - 缺点:生成的二进制体积通常比仅
IL
更大;缺少JIT
运行时才能做的某些动态优化;对反射、动态代码(如System.Reflection.Emit
、某些动态库调用)支持有限,需要额外配置。
.NET 中主要的 AOT 形式
ReadyToRun(R2R)
- 原理:
.NET Core 3.0+
引入的预编译方案,通过crossgen
或crossgen2
工具,将IL
打包成一种混合格式:既包含IL
,也包含部分已编译的本机代码片段。运行时遇到已编译的本机代码就直接执行,未编译部分仍可JIT
。 特点:
- 部署包仍然包含
IL
,因此仍需要CLR
支持执行IL
; - 与纯
JIT
相比,能显著缩短冷启动时间; - 兼容性较好,不会因为反射动态调用导致编译失败;
- 可通过项目文件中
PublishReadyToRun=true
启用。
- 部署包仍然包含
- 使用示例(
.NET 6/7
均适用)
<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
<!-- 可以指定针对某一架构:anycpu、x64、arm64 等 -->
<PublishReadyToRunUseCrossgen2>true</PublishReadyToRunUseCrossgen2>
</PropertyGroup>
运行
dotnet publish -c Release -r win-x64
优缺点:
- 优点:相对于纯
IL
包体积增量较小;兼容性强;启动速度提升显著。 - 缺点:仍需
CLR
支持;对于某些存在大量泛型/反射调用的应用,如果R2R
编译时未覆盖,运行时会有JIT
编译;对发布包体积有一定增加。
- 优点:相对于纯
Native AOT(以前称为 CoreRT、.NET Native)
- 原理:自
.NET 7
起,官方推出了基于 “Native AOT
” 的技术分支,可以将应用编译为真正的本机可执行文件,省去了运行时(CoreCLR
)依赖。Native AOT
在编译阶段会对所有可达代码(包括泛型实例化、已知反射调用等)进行全局分析,并生成极致优化的机器码,同时将垃圾回收、类型元数据等必要组件整合进单一可执行文件或较少的DLL
。 特点:
- 最终输出为原生可执行文件,运行时无需安装
.NET
运行时; - 启动速度和内存使用均优于
JIT
或R2R
; - 可实现瘦二进制(使用“修剪”(
trimming
)技术去除未使用的程序集和类型); - 对反射、动态代码支持有限,需要手动配置
rd.xml
或TrimmerRootAssembly、DynamicDependency
等显式保留信息; - 目前仅支持 控制台应用/单一可执行体,不支持
ASP.NET Core
完整框架(仅支持最小化API
)。
- 最终输出为原生可执行文件,运行时无需安装
- 使用示例
<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
(或对应平台无后缀可执行文件)。
优缺点:
- 优点:运行时零依赖、启动极快、内存占用低、体积可控(借助修剪);
- 缺点:不支持某些 “运行时动态” 场景(如
Reflection.Emit
、动态加载插件、ScriptEngine
等);对反射访问的类型/成员必须在编译时预声明,否则会被修剪;调试体验不如JIT
(需要额外符号文件);生态兼容度仍在完善中。
Mono AOT(Xamarin / Unity 场景)
- 原理:
Mono
提供的AOT
功能,可以在iOS/macOS/Android
等平台上将IL
预编译为机器码,避免目标平台禁止JIT
的限制(特别是在iOS
上)。 特点:
- 主要应用于移动端(
Xamarin.iOS
、Unity iOS
构建等); - 编译过程会在打包时将
IL
转为目标架构的本机代码; - 对大多数标准库和第三方库支持较好,但执行时会额外加载
Blitz
或Mono
运行时支持(并非完全剥离)。
- 主要应用于移动端(
使用示例:
- 在
Xamarin.iOS
项目中,默认Release
模式下会启用AOT
;也可在项目属性中手动开启“Enable LLVM
”和“AOT Only
”(仅AOT
)。 - 在
Unity
构建iOS
时,也会默认将脚本代码以AOT
模式编译。
- 在
优缺点:
- 优点:符合
iOS
平台安全/性能需求; - 缺点:包体积增大;使用某些需要运行时生成
IL
的库会失败。
- 优点:符合
AOT 在 .NET 中的演进与对比
特性 / 版本 | .NET Core 3.x & .NET 5/6 R2R | .NET 7/8 Native AOT | Mono 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
加快冷启动
- 对于启动时间敏感的应用(如命令行工具、微服务、
Serverless
函数、IoT
设备、移动端应用等),AOT
能显著减少启动时等待JIT
编译的开销。
- 对于启动时间敏感的应用(如命令行工具、微服务、
减少运行时依赖
Native AOT
可将运行时(CoreCLR
)与垃圾回收等程序集成到一个可执行文件里,实现“零依赖”部署。对于需要极简体积或目标环境不允许安装.NET
运行时的场景(比如无管理员权限的服务器、Linux
发行版中未安装.NET、Docker
镜像瘦身需求),非常有帮助。
提高性能可预测性
- 因为所有代码都在发布前编译完成,运行时不会有突然的
JIT
阶段,尤其在高并发场景下,避免了突发的编译延迟或CPU
峰值。 - 对于资源受限的环境(嵌入式、边缘计算设备),可减少
JIT
引发的内存和CPU
瞬时占用。
- 因为所有代码都在发布前编译完成,运行时不会有突然的
安全性 / 平台限制
- 某些平台(如
iOS
)不允许运行时生成机器码(即禁止JIT
),必须使用AOT
。Mono AOT
以及Xamarin
都基于此需求在iOS
平台默认强制AOT
。
- 某些平台(如
二进制可移植性
- 在
Native AOT
下,可将生成的可执行文件拷贝至目标环境直接运行,无需在目标环境重新编译,提升交付效率。
- 在
AOT 实现的关键流程
扫描可达程序集
- 编译器(
IL to object
)需要先扫描所有引用的程序集,收集根节点(Main
方法、动态库加载点、反射需求等)。 - 通过
IL Linker
(修剪器)算法,对树状可达性做静态分析。对于未标记为可达的代码进行剔除,从而减小体积。
- 编译器(
生成本机代码
- 将
IL
转换为中间的“中间表示”(如RyuJIT IR
或LLVM IR
),并通过平台本地编译器(例如MSVC、LLVM
)优化后生成对应体系结构的机器码。 - 同时将
GC
、类型元数据、异常处理表等运行时信息打包到可执行文件中。
- 将
处理反射 / 动态需求
- 因为编译时无法窥见运行时可能使用的反射类型,需要开发者通过属性或
XML(.rd.xml)
声明“需要保留”的类型/程序集/成员,否则编译器会在做“修剪”时误删这些代码。 .NET 7+
用DynamicDependencyAttribute、PreserveDependency
等方式告知编译器保留反射访问所需类型。
- 因为编译时无法窥见运行时可能使用的反射类型,需要开发者通过属性或
生产最终可执行文件
- 静态地将机器码、元数据、运行时库等整合成一个单一文件,或者如
Linux
下分为可执行 + 一些.so
文件。 - 通过
dotnet publish -c Release -r <RID> /p:PublishAot=true
完成。
- 静态地将机器码、元数据、运行时库等整合成一个单一文件,或者如
如何在项目中启用 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
注意:
- 如果项目中使用到了反射(例如
Activator.CreateInstance
、Type.GetType、JsonSerializer
等),编译时需要添加对应的保留配置,否则会出现无法找到类型或成员的运行时异常。 - 对于依赖第三方
NuGet
包且该包使用了动态特性,也需要检查是否AOT
兼容;部分库需要手动编写TrimmerRootAssembly
或自定义rd.xml
。
AOT 的优缺点及适用场景
优点
极快启动:
- 省去运行时
JIT
阶段,首屏响应或启动速度几乎与本机程序无差异。
部署简单:
Native AOT
模式下可执行文件自包含所有依赖,无需目标机器预先安装.NET
运行时。
更小内存占用峰值:
- 通过修剪技术剔除未使用的代码和依赖,运行时加载更轻量。
可预测走向发布:
- 编译时已做全部优化与检查,减少运行期“编译出错”或动态缺失依赖的问题。
符合某些平台限制:
- 比如在
iOS
平台禁止JIT
,通过Mono AOT
或Xamarin iOS
编译可满足Apple
审核要求。
缺点
体积膨胀 vs 兼容性:
ReadyToRun
相对于纯IL
包增量并不大,但Native AOT
若不精心修剪,体积可能与完整运行时相当;- 某些第三方库对
AOT
支持不好,可能需要额外适配。
有限的运行时代码生成:
- 无法做
Reflection.Emit
、动态生成表达式树等,需要在编译期预先声明; - 运行时使用
System.Text.Json、Newtonsoft.Json
等反射型序列化/反序列化时,需手动配置JsonSerializerContext
或显式注册要序列化的类型。
调试和诊断不便:
Native AOT
下StackTrace
可能缺少符号映射;- 如出现
CPU
性能或内存泄漏问题,无法借助JIT
时代的动调。
不适合大型动态场景:
- 如果项目本身依赖插件热加载、脚本引擎、或者大量运行时元编程,就不适合
Native AOT
。
典型适用场景
命令行工具(CLI)
- 比如一些
Git
扩展工具、DevOps
脚本工具、跨平台部署时,Native AOT
可做到零依赖、秒级启动。
微服务/Serverless 函数
- 在容器或云函数中,冷启动时间至关重要;使用
AOT
或R2R
可降低冷启动延迟。
桌面/移动端轻量应用
- 某些场景下需要小体积、无运行时依赖,或目标平台禁止
JIT
,可考虑AOT
。
IoT/嵌入式设备
- 在资源受限的硬件上,减少运行时占用,提升响应速度。
创建一个 Native AOT 控制台应用
创建项目
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.exe
或 AotDemo
运行与验证
直接双击或命令行执行 AotDemo.exe
或 ./AotDemo
,输出:
Hello Native AOT World!
当前类型:AotDemo.Program
分析产物体积
- 若没有配置修剪(
<PublishTrimmed>true</PublishTrimmed>
),可执行文件体积约在 30–50MB 左右。 - 启用修剪后,一般可以减小到 10–20MB(视代码复杂度及所引用包而定)。
如果使用了反射
- 在
Native AOT
中直接调用typeof(Program)
读取自身类型是可以的,因为编译器会保留Program
类; - 如果反射调用一个仅在运行期才可确定的类型(例如字符串拼接得到的类型名称),编译时无法知道,就需要在项目里添加
rd.xml
或使用DynamicDependency
属性显式声明保留该类型。
诊断和调试
- 对于
Native AOT
,调试体验不如JIT
平滑,特别是断点、StackTrace、Debug.Assert
之类需要符号的场景。 - 建议:仅在开发阶段默认关闭
AOT
,保留JIT
类库的常规调试;发布前再切换至AOT
。
rd.xml 配置文件
作用
在 .NET Native AOT、ReadyToRun
+ 修剪(ILLinker
)等场景中,编译器或 IL
链接器会在发布阶段对中间语言(IL
)进行分析与“修剪”(Trim
),剔除未被静态调用或引用的类型、成员与元数据,以减小输出体积、提升启动性能。然而,反射(Reflection
)是一种运行时特性:代码中的某些类型、方法、属性等只有在运行阶段通过反射动态访问,编译时并不可见。若不做额外保留,ILLinker
在静态分析时会误判这些成员为“不可达”,进而被剔除,导致在运行时使用诸如 Activator.CreateInstance、Type.GetType
、序列化/反序列化、ORM
映射等场景抛出 “找不到类型/成员” 的异常。
.rd.xml
文件是 .NET Native
与 ILLink
场景下的“保留指令”描述文件(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 Name="..." [Dynamic="..."] [Serialize="..."]>
:标记要保留的程序集,以及对该程序集下类型的保留策略。<Type Name="..." [Dynamic="..."] [Serialize="..."]>
:在某个<Assembly>
内部,用于配置要保留的类型(类、接口、结构体、枚举等)。常见的属性:Dynamic="Required All"
:保留该类型的所有成员(字段、属性、方法、事件等)以满足动态访问。Serialize="Required Public"
:仅保留该类型公共可序列化成员,供 JSON/XML 序列化时使用。
<Method Name="..."、<Field Name="..."、<Property Name="..."
等子节点:进一步精细到单个成员级别的保留配置。
常用配置项说明
在 <Assembly>
和 <Type>
节点上,常见的属性及含义如下:
Dynamic="Required All" / "Required Public" / "Required PublicAndCritical" 等
Required All
:保留该节点下所有成员(字段、属性、方法、事件等,无论是否为public
或private
),用于某些场景需要完全动态访问。Required Public
:仅保留公共(public
)成员。Required PublicAndCritical
:保留公共成员以及具有安全Critical
特性。
Serialize="Required Public" / "Required All"
- 主要用于
JSON/XML
序列化场景:保留类型的公有字段与属性,以便在运行时的序列化/反序列化机制(如System.Text.Json
或XmlSerializer
)能够正常工作。 - 与
Dynamic
同时存在时,序列化场景保留的成员更加精准(避免把每个私有成员都打包进二进制)。
- 主要用于
Collections
- 如果类型中有对泛型集合(
List<T>
)的反射访问(例如Activator.CreateInstance(typeof(List<>).MakeGenericType(...)))
,需要通过GenericInstantiation
子节点显式声明泛型实例化需求。
- 如果类型中有对泛型集合(
Version / Culture / PublicKeyToken
<Assembly Name="Name" Version="1.0.0.0" Culture="neutral" PublicKeyToken="abcdef1234567890">
- 当引用了特定强命名程序集中类型,需要写明版本号与公钥令牌,才能准确匹配。若不写
Version/Culture/PublicKeyToken,ILLink
会尝试做宽松匹配(仅匹配Name
)。
.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
)进行反射保留:
<?xml version="1.0" encoding="utf-8"?>
<Directives xmlns="http://schemas.microsoft.com/netcore/2013/01/metadata">
<Application>
<Assembly Name="MyApp.Core">
<!--
保留指定类型 PluginLoader 及其所有成员(无论 public/private)
“MyApp.Core.Services.PluginLoader” 为类型完全限定名。
-->
<Type Name="MyApp.Core.Services.PluginLoader" Dynamic="Required All" Serialize="Required Public" />
</Assembly>
</Application>
</Directives>
若该类型是泛型类型,例如 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>
- 其中反引号后 1 表示一参泛型;
<GenericInstantiation>
内指定了要实例化的具体泛型参数类型,必须给出该类型的程序集、版本、Culture、PublicKeyToken 等完整信息;否则无法被正确保留。
保留 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>
<Method>、<Field>、<Property>
节点均需要写上 Name;Dynamic
值可针对该节点保留范围进行调整。
在项目中集成 .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>
TrimmerDefaultAction
:link
表示默认修剪所有未被标记为保留的代码;copy
表示复制所有引用的程序集但不修剪(相当于R2R
模式);
TrimmerRootDescriptorFiles
:指定一个或多个.rd.xml
文件路径,多个文件以分号分隔。这里使用相对路径rd.xml
。
编译与发布命令
dotnet publish -c Release
编译器会自动读取 rd.xml
,根据其中配置的保留指令对代码进行修剪,确保运行时反射需求的类型与成员被正确保留。
调试与验证
- 启用链接器诊断日志
在 .csproj
中添加或在命令行指定:
<PropertyGroup>
<!-- 打印链接器日志到指定文件 -->
<TrimmerLogFile>trim-log.xml</TrimmerLogFile>
<!-- 显示链接器分析的详细级别(可选:Skip、Silent、Verbose、diagnostic) -->
<TrimmerLogLevel>diagnostic</TrimmerLogLevel>
</PropertyGroup>
发布后会在输出目录生成 trim-log.xml
,打开后查找是否有某个类型/成员被修剪或被保留的记录。诊断级别输出会非常详细,便于查找“保留”是否生效,或哪些类型因遗漏而被剔除。
运行时测试反射调用
- 在发布后拷贝到干净环境,手动调用关键反射逻辑,验证是否抛出
TypeLoadException、MissingMethodException
等。 - 如果依旧报错,查看
trim-log.xml
中对应类型/成员是否被剔除,若剔除则需要在rd.xml
做进一步保留。
- 在发布后拷贝到干净环境,手动调用关键反射逻辑,验证是否抛出
使用
ILSpy / dotnet-ildasm
工具检查输出- 可对生成后的可执行文件或
.dll
在反编译工具(ILSpy、dnSpy
)中查看某些类型是否还存在。 - 或者用
dotnet-ildasm
导出元数据,再搜索对应类型/成员名。
- 可对生成后的可执行文件或
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。