亭立 發表於 2025-6-12 10:02:28

.NET AOT 详解

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