.NET Native AOT 是什么 - 与 JIT、trimming 的区别

· 更新日期: · · C#, .NET, Native AOT, 发布, 设计

更新记录(1 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.21615453)
首次发布
引用本文(DOI: 10.5281/zenodo.21615452)

本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。

小村 豪(2026)。《.NET Native AOT 是什么 - 与 JIT、trimming 的区别》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615452 https://comcomponent.com/zh-CN/blog/2026/03/13/001-dotnet-native-aot-what-is/

DOI(最新版本)
10.5281/zenodo.21615452
DOI(此版本)
10.5281/zenodo.22281970

在 从 C/C++ 调用 C# Native AOT DLL 的方法 里,已经写过用 Native AOT 从 C/C++ 调用 C# 的做法。 不过说实话,更周到的顺序是先放一篇 Native AOT 究竟是什么。前后稍微颠倒了一点。

谈 Native AOT,一开始最容易出问题的就是术语混在一起。

  • 这是在讲去掉 JIT 吗
  • 和 self-contained、single-file 有什么不同
  • 和 ReadyToRun 是同一路的吗
  • trimming warning 大量冒出来,到底发生了什么
  • 在 WPF / WinForms / ASP.NET Core 上是否也能以同样的力度使用

这些一旦混为一谈,Native AOT 看起来就会像“只是变快的魔法”,或者反过来像“全是限制、很可怕的东西”。两种看法都有点粗糙。

术语混在一起会造成的误解JIT、self-contained、ReadyToRun、trimming这些术语混为一谈时,Native AOT看起来会像只是变快的魔法,或者反过来像全是限制的可怕东西,但两种看法都很粗糙。术语混为一谈看起来像变快的魔法看起来像全是限制、很可怕把词分开梳理变成用途清晰的选项

图1:误解的根源是术语混线。先从把词分开开始。

本文以 .NET 8 之后的当前实务感受为前提,先整理下面四点。

  • Native AOT 到底是什么
  • 好处在哪里,哪里会变吃力
  • 与 ReadyToRun、trimming 有什么不同
  • 从什么样的应用开始试比较稳

目录

  1. 先说结论(一句话)
  2. 先看的对照表
    • 2.1. Native AOT 周边的术语
    • 2.2. JIT / ReadyToRun / Native AOT 的区别
  3. Native AOT 的整体图景(图)
  4. Native AOT 能带来什么好处
    • 4.1. 启动容易变轻
    • 4.2. 不必以预装运行时为前提
    • 4.3. 适合受限的运行环境
  5. Native AOT 会在哪些地方变吃力
    • 5.1. 反射与动态代码生成
    • 5.2. 必须以 trimming 为前提来考虑
    • 5.3. 按平台分别发布
    • 5.4. Windows 桌面 / COM 场景要相当谨慎
  6. 最小步骤
    • 6.1. csproj
    • 6.2. publish
    • 6.3. JSON 的写法
  7. 适合的场景
  8. 不适合的场景
  9. 容易踩的坑
  10. 总结
  11. 参考资料

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 16 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

1. 先说结论(一句话)

  • Native AOT 是在 publish 时把 .NET 应用预先编译成原生代码再分发的方式。
  • 因为不使用运行期 JIT,启动时间和内存占用容易变好,也更容易分发到没有安装 .NET 运行时的环境。
  • 但代价是,与自由的反射、动态代码生成、built-in COM 以及不支持 trimming 的库都不合拍。
  • 也就是说,它与其说是让程序变快的魔法,不如说是为了启动、发布、运行环境上的需要,舍弃一部分动态世界、向静态世界靠拢的发布模型。

Native AOT 是“把 .NET 以接近原生的方式分发出去的机制”,而不是一个单纯让编译变快的勾选框。

得到什么、舍弃什么Native AOT通过publish时的预先编译换来启动时间、内存占用和无需运行时的分发,代价是舍弃自由的反射、动态代码生成以及与built-in COM的兼容,是这样一种发布模型。Native AOT得到的东西舍弃的东西启动、内存与分发的轻量自由的反射动态代码生成与built-in COM

图2:不是变快的魔法,而是舍弃一部分动态世界、向静态世界靠拢的一笔交换。

2. 先看的对照表

2.1. Native AOT 周边的术语

先把这几个词分清楚,后面会轻松很多。

术语 做的是什么 与 Native AOT 的关系
JIT 在运行期从 IL 生成原生代码 Native AOT 把这一步提前做完
self-contained 把运行所需的 .NET 一整套一起分发 Native AOT 归在这一路来考虑
single-file 把发布产物合并成 1 个文件 与 Native AOT 的本质是两回事,但结果的外观容易接近
trimming 删掉没用到的代码 在 Native AOT 里基本上是前提
ReadyToRun 保留 IL,把 JIT 的工作稍微提前 与 Native AOT 貌似而实非,是另一样东西
source generator 把运行期的动态处理挪到构建期代码生成 与 Native AOT 配合良好

容易混淆的地方在于,Native AOT 与其说是一个功能,不如说是一种与 self-contained、trimming、source generation、固定 RID 的 publish 等配合运作的发布模型。

与Native AOT一起运作的机制说明Native AOT不是单独的一个功能,而是与self-contained、trimming、source generation、固定RID的publish组合起来运作的发布模型。Native AOT〔发布模型〕self-contained这一路trimming基本是前提与source generator配合良好固定RID后publish

图3:Native AOT 不是单独的功能,而是与周边机制成套运作的发布模型。

2.2. JIT / ReadyToRun / Native AOT 的区别

这里也是先用一张表看完更快。

角度 普通的 JIT 执行 ReadyToRun Native AOT
运行期 JIT 使用 仍有会用到的场合 不使用
发布产物的内容 以 IL 为主 IL + 事先生成的代码 以原生可执行文件为主
启动 基准 容易改善 相当容易改善
兼容性 最广 广 限制较强
动态功能 好用 大体上好用 限制较多
适合的目的 一般的 .NET 开发 首先想改善启动 强力争取启动、分发、受限环境

如果说 ReadyToRun 是“让 JIT 轻松一点”的方向,那么 Native AOT 就是“不以运行期 JIT 本身为前提”的方向。 虽然都带着 AOT 这个词,力度却相差很多。

ReadyToRun与Native AOT的方向差异ReadyToRun是保留IL、把JIT的工作稍微提前从而让JIT轻松一点的方向,Native AOT是不以运行期JIT本身为前提的方向,虽然都叫AOT,力度却相差很多。让它轻松一点不作为前提想把JIT怎么办ReadyToRun〔IL保留〕Native AOT〔以原生为主〕兼容性依然很广启动快但限制强

图4:同样是 AOT,“让 JIT 轻松”和“不以 JIT 为前提”是两回事。

在此基础上,把“那到底该选哪种分发形态”画成一张图,就是下面这样。

能装没到那种程度想压缩不想装 / 装不了依赖不依赖是否想确定分发形态能在目标环境装 .NET 运行时吗?想压缩启动时间吗?framework-dependent普通发布ReadyToRun保持兼容性的同时改善启动依赖 reflection、动态代码生成、built-in COM 吗?self-contained需要时用 single-file 打包对象是 console / worker / 小型 API 吗?Native AOT先 self-contained减少动态依赖后再重新评估

图5:分发形态的选法。先看能不能放运行时,再看能不能减少动态机制。

第一个分支是 能不能在目标环境放置运行时,第二个分支是 动态机制能减少到什么程度。 self-contained 与 single-file 讲的是“怎么分发”,ReadyToRun 与 Native AOT 讲的是“什么时候生成原生代码”,所以实际上要组合起来考虑。

3. Native AOT 的整体图景(图)

把 Native AOT 粗略画成图,就是下面这样。

通常执行dotnet publish + PublishAotC# / .NET 源代码IL 程序集运行期 JIT应用运行AOT / trim 分析削减不需要的代码生成原生代码RID 专用的可执行文件

图6:通常执行是在运行期做 JIT,而 Native AOT 把分析、削减、原生代码生成都提前到 publish 时。

平时的 .NET 先生成 IL,再在运行期只对需要的部分做 JIT。 Native AOT 把其中后半段的大部分工作提前到 publish 时。

这里关键的一点是,在 publish 时点“必须几乎全部知道运行期会用到哪些代码”。 于是,允许什么样的写法,前提就变了。

  • 在运行期查找类型
  • 在运行期生成代码
  • 在运行期加载 Assembly
  • 在运行期抱着“总能糊弄过去”的心态做延迟解析

这类写法,一遇到 Native AOT 就会突然变得不合拍。

publish时必须已经全部知道Native AOT在publish时点必须几乎全部知道运行期会用到的代码,因此与运行期查找类型、运行期生成代码、运行期加载Assembly、靠延迟解析蒙混过去这类写法都不合拍。publish时确定必要代码运行期查找类型运行期生成代码运行期读取Assembly靠延迟解析应付这些都与AOT不合拍

图7:前提改变的核心。越是“运行期看了再决定”的写法,越容易和 Native AOT 冲突。

4. Native AOT 能带来什么好处

4.1. 启动容易变轻

Native AOT 最直观的效果,还是启动。

  • CLI 工具
  • 短生命周期进程
  • 类似 serverless 的启动
  • 容器的启动与替换
  • 监控工具和小型常驻进程

在这些场景里 JIT 的开销很容易显现,用 Native AOT 把这部分提前做完,起步就会更轻。

内存占用也容易改善,因此在想把同样数量的实例塞进有限资源的场合同样有利。 特别是在会大量拉起同一个进程的云端,这点差距会慢慢显现出来。

4.2. 不必以预装运行时为前提

用 Native AOT publish 出来的应用,在没有安装 .NET 运行时的环境里也更容易跑起来。

这一点看着不起眼,其实影响很大。

  • 不想对使用方说“请先装好 .NET 9 Runtime”
  • 想把容器镜像做瘦
  • 只想放一个小工具就让它跑起来
  • 不想在运行环境上允许 JIT,或者根本不被允许

在这些场合里,只要没有“还得另外准备运行时”这个前提,事情就会安静很多。

这里说的“不需要运行时”,指的是不必在目标环境另外安装 .NET。 并不是说应用内部连相当于 runtime 的必要部分都完全消失了。

不需要运行时的正确含义Native AOT说的不需要运行时是指不必在目标环境另外安装.NET,并不是说应用内部连相当于runtime的部分也完全消失了。不需要运行时这个说法不必在目标环境另装.NET并不是runtime部分消失了分发与启动的前提变少

图8:“不需要运行时”说的是目标环境。相当于 runtime 的部分仍然包含在应用里。

4.3. 适合受限的运行环境

Native AOT 不使用运行期 JIT,所以在不允许 JIT 的环境里也更容易运行。

这一点比起 desktop,更容易在云、容器、移动端这类场景里发挥作用。 不过即使在 Windows 开发的语境下,从“减少目标环境上多余的前提条件”这个意义上说,它也足够让人高兴。

5. Native AOT 会在哪些地方变吃力

5.1. 反射与动态代码生成

Native AOT 最核心的限制就在这里。

  • 像 Assembly.LoadFile 这样的动态加载
  • 像 System.Reflection.Emit 这样的运行期代码生成
  • 在运行期无限制地遍历类型的反射
  • 在运行期随心所欲拼装 generic 的写法

这些写法在 publish 时不容易把必要代码固定下来,因此会成为 AOT warning 的温床。

当然,事情没有简单到“只要有一行反射就立刻出局”。 不过,越是偏向“运行期看了再决定”的设计就越吃力,这个倾向不会错。

warning 名里经常出现的是 RequiresDynamicCode 系列。 它的意思是“这个调用在 AOT 下可能会坏掉”,所以不要草率地 suppress 更安全。

做 Native AOT 的时候,可以记成:减少“运行期的聪明”,增加“构建期的显式声明”,这样理解起来比较清楚。

从运行期的聪明转向构建期的显式声明动态加载、运行期代码生成、无限制的反射都是AOT warning的温床,因此要减少运行期的聪明,把设计推向增加构建期显式声明的方向。依赖运行期聪明的设计RequiresDynamicCode系的warning增加构建期的显式声明publish时确定必要代码不要草率地suppress

图9:面对核心限制的方向只有一个。把“运行期看了再决定”换成“构建期明确写出来”。

5.2. 必须以 trimming 为前提来考虑

Native AOT 与 trimming 结合得很紧。 这里容易被忽略的是,起作用的不只是自己的代码,还有依赖库那边的写法。

需要注意的是下面这些。

  • 以反射为基础的序列化器
  • 靠运行期扫描收集类型的 DI / 插件结构
  • 从字符串名查找类型再实例化的机制
  • 偏向动态 proxy 或 IL 生成的库

明明这里冒出了 warning,却以“publish 通过了就行”收场,后面会相当难受。 在 Native AOT 里,warning 基本上都值得认真读。

不只由自己的代码决定的trimmingtrimming不只自己的代码起作用,依赖库的写法也会起作用,以反射为基础的序列化器、靠运行期扫描的DI、从字符串名解析类型、偏向动态proxy的库都需要注意。trimming的成败自己代码的写法依赖库的写法反射系与运行期扫描系认真读warning并核实

图10:容易漏掉的是依赖库那边。“publish 通过了就行”后面会变难受。

话虽如此,只说“认真读”还是没法动手,所以这里写下处理的优先顺序。 官方文档也建议按这个顺序尝试。

顺序 处理方式 使用场合
1 不再使用 reflection 能把 Activator.CreateInstance(Type) 换成 generic 参数,或者能挪到 source generator
2 加上 DynamicallyAccessedMembers reflection 确实需要,但目标类型在编译期已经确定
3 加上 RequiresUnreferencedCode 用运行期的字符串决定类型名等,本质上无法做静态分析
4 用 UnconditionalSuppressMessage 抑制 上面几种全都行不通、且已确认安全时的最后手段

作为第一步,最常见的是第 2 种,也就是“类型明明已经确定,却只是冒出警告”的情况。 例如,下面这段代码会报 IL2070。

// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

只要用特性声明“既然要调用 GetMethods(),就请保留 public 方法”,警告就会消失。

using System.Diagnostics.CodeAnalysis;

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// 只要调用方是用 typeof 传进来的,这个要求就会自动得到满足
PrintMethodNames(typeof(DateTime));

调用的 API 与需要的指定大体上是一一对应的。GetMethod / GetMethods 对应 PublicMethods,GetProperty / GetProperties 对应 PublicProperties,Activator.CreateInstance 对应 PublicParameterlessConstructor 或 PublicConstructors。 DynamicallyAccessedMemberTypes.All 写起来省事,但会把目标类型的成员整个保留下来导致体积膨胀,而且保留下来的成员还会带出别的 warning,所以原则是 只指定必要的最小范围。

另外,加了特性 warning 仍然不消失时,要 从使用 reflection 的地方向调用方回溯,确认整条路径上都加上了特性。中间只要少一处,要求就会在那里断掉。

加了特性warning仍不消失时DynamicallyAccessedMembers要按必要的最小范围指定,warning不消失时要从使用reflection的地方向调用方回溯,确认整条路径上都加了特性。中间少一处,要求就会在那里断掉。加了特性warning仍然残留向调用方回溯确认整条路径都加上了少一处要求就在那里断掉指定以必要的最小范围为原则

图11:特性要加在整条路径上,而不是加在某一个点上。中间少一处,要求就在那里断掉。

5.3. 按平台分别发布

Native AOT 是在固定 RID(Runtime Identifier)的前提下 publish 的。 也就是说,这里不是那种把为 win-x64 生成的产物原样拿到 linux-x64 上运行的世界。

  • Windows x64
  • Windows Arm64
  • Linux x64
  • Linux Arm64
  • macOS Arm64

像这样,前提是按每个目标分别生成发布产物。

这一点比起普通的 framework-dependent 的 .NET,感觉会明显更“像原生应用”。

5.4. Windows 桌面 / COM 场景要相当谨慎

放在 KomuraSoft 的语境里,这一点尤其重要。

在 Windows 上,Native AOT 没有 built-in COM。 再加上 WPF 与 trimming 配合不好,WinForms 对 built-in COM marshalling 的依赖很重,所以至少在当前阶段,把这两者当作“第一批 Native AOT 候选”都需要相当谨慎地看待。

这里把所说的“当前”的基准写清楚。 本文的描述基于 .NET 8 / 9 / 10 这几代的官方文档(截至 2026 年 7 月)。 关于 WPF 和 WinForms,微软的“Known trimming incompatibilities”中明确写道:WPF 对 reflection 和运行期代码检查的依赖很强,trimming 之后基本无法运行;WinForms 对 built-in COM marshalling 的依赖很重,因此两者都已在 .NET SDK 侧关闭了 trimming 支持。 也就是说,现状不是“最好谨慎看待”,而是 SDK 直接拦住了。将来这段描述有可能变化,请在阅读时确认同一个页面。

WPF与WinForms被拦住的理由Windows的Native AOT没有built-in COM,WPF因为对reflection和运行期代码检查的依赖很强在trimming之后基本无法运行,WinForms因为对built-in COM marshalling的依赖很重,两者都已在.NET SDK侧关闭了trimming支持。Windows的Native AOT没有built-in COMWPFreflection依赖很强WinFormsCOM marshalling依赖很重SDK侧关闭了trimming

图12:不是“要谨慎”,而是现状由 SDK 拦住。桌面主体不要当作第一批候选。

总之,

  • 一上来就把 WPF / WinForms 主体改成 Native AOT
  • 按平时的感觉把 COM interop 原样带进来

这些做法会让 publish 时的 warning 和运行期的限制一下子增多,应对成本容易暴涨。

反过来,

  • 控制台程序
  • worker
  • 小型 Web API
  • 在原生互操作里也容易收敛到 C 函数边界的组件

这些作为入口更自然。

如果确实需要 COM,有些场合下保持 JIT,或者以 ComWrappers / source-generated COM 为前提重新设计,反而更合理。

6. 最小步骤

在此之前,Native AOT 的 publish 需要原生工具链。 只加了 PublishAot 就执行 dotnet publish,失败的不是编译,而是最后的原生链接阶段。这是第一道关卡,请先把它装好。

环境 需要的东西
Windows Visual Studio 2022 以上。安装“使用 C++ 的桌面开发”工作负载,并把默认组件全部选上
Ubuntu 18.04 以上 sudo apt-get install clang zlib1g-dev
Alpine 3.15 以上 sudo apk add clang build-base zlib-dev
Fedora 39 以上 / RHEL 8 以上 sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel
macOS Xcode 的 Command Line Tools(.NET 8 以后支持)

说白了就是编译器工具链,以及 .NET 运行时所依赖的库的开发用软件包。 如果要在 CI 上跑,dotnet/samples 的 Native AOT 示例里同时放了 Linux / Windows 两种 Dockerfile,从那里照搬前提条件的安装步骤最快。

另外,在 Linux 上构建的二进制 只能在相同或更新的 Linux 上运行。在 Ubuntu 20.04 上构建的产物可以在 20.04 及以后运行,但在 18.04 上跑不起来。构建环境怎么选,直接决定了可分发的范围。

publish前的关卡与可分发范围Native AOT的publish需要原生工具链,没有的话会在最后的原生链接阶段失败。另外在Linux上构建的二进制只能在相同或更新的Linux上运行,构建环境的选法直接成为可分发范围。没有有有没有工具链在原生链接阶段失败产出按RID区分的二进制Linux只在相同或更新的环境上运行构建环境的选法即分发范围

图13:第一道关卡是工具链。在 Linux 上,构建环境有多旧决定了可分发的范围。

6.1. csproj

首先在项目文件里加上 PublishAot。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

示例用 net8.0 就够了。思路本身在 .NET 9 / 10 上也基本相同。

重要的是,与其只在 dotnet publish 的命令行上临时加一下,不如 平时就把它放在项目里,日常查看 build / publish 时的分析结果。

另外,即使加上 <PublishAot>true</PublishAot>,平时的本地运行也不会立刻变成 Native AOT。 日常的 dotnet run 和普通执行仍然走 JIT,Native AOT 编译的正戏在 publish 时。

加入PublishAot之后的日常即使把PublishAot放进项目,日常的dotnet run仍然走JIT,Native AOT编译的正戏在publish时才跑。平时就把它放在项目里,日常查看build与publish时的分析结果很重要。在csproj里放入PublishAot日常的dotnet run仍是JITpublish时进行AOT编译日常查看分析与warning

图14:日常走 JIT,正戏在 publish。正因如此,配置不要临时加,而要常驻并持续查看分析结果。

6.2. publish

例如面向 Windows x64 时,是这样。

dotnet publish -c Release -r win-x64

面向 Linux x64 时,是这样。

dotnet publish -c Release -r linux-x64

输出是固定 RID 的。 看待方式会从“一份到处都能跑的 .NET DLL”,变成“为那个 OS / 架构专门生成的可执行文件”。

如果从 Web API 这边入手,从以 Native AOT 为前提的模板开始会更省事。

dotnet new webapiaot -o MyFirstAotWebApi

worker 的话用这个。

dotnet new worker -o WorkerWithAot --aot

6.3. JSON 的写法

在 Native AOT 里意外常撞上的是 JSON。 System.Text.Json 按平时的感觉使用容易滑向 reflection,因此挪到 source generation 会更稳当。

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

public sealed class AppConfig
{
    public string? Name { get; init; }
    public int RetryCount { get; init; }
}

var config = new AppConfig
{
    Name = "sample",
    RetryCount = 3
};

string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);

在实务中,与其记成“为了支持 Native AOT”,不如记成 往“不让运行期去查找类型”的方向靠,这样不容易跑偏。

7. 适合的场景

Native AOT 用起来最舒服的,是下面这些场景。

  • 以启动为主角的 CLI / 工具
  • 在容器里大量部署的小型 API
  • worker / 后台服务
  • serverless 或短生命周期进程
  • 嵌入原生应用里的小型 .NET 组件
  • 不想要求运行环境预装 .NET 运行时的场合

它们的共同点是,边界相对清晰,动态机制容易减少。

8. 不适合的场景

反过来,也有一些场合明确不适合一开始就把 Native AOT 当作主战场。

  • WPF / WinForms 既有大型应用的主体
  • 以 built-in COM interop 为前提的结构
  • 以运行期加载 plugin 为主角的应用
  • 强依赖用 reflection 搜索类型的框架的结构
  • 把 System.Reflection.Emit 和动态 proxy 用得理所当然的库
  • 中间夹了 C++/CLI 的设计

这些情况下,以 JIT 为前提的普通 .NET、或者 ReadyToRun、或者重新梳理设计的边界划分,都更合理。

9. 容易踩的坑

最后,整理一下上手 Native AOT 时最容易踩的点。

  • 轻视 publish warning
    • 如前所述,Native AOT 的 warning 是需要认真读的对象。
  • build 能过,publish 却坏掉
    • publish 时会把依赖库也包含进来认真跑一遍分析,所以有些问题到这一步才第一次显现。
  • 用同一种心态对待 ReadyToRun 和 Native AOT
    • 词看着相似,限制的强度却相差很多。
  • 一上来就从 desktop 应用主体开始
    • 先从 console / worker / 小型 API 入手更稳。
  • 按平时的习惯写 JSON 和配置绑定
    • 以 reflection 为前提的写法,后面会反过来发作。
  • 以为与平台无关就直接分发出去
    • Native AOT 的发布产物是固定 RID 的。
  • 以为“Native AOT = 什么都会变快”
    • 主角是启动、分发、运行环境。看错这一点,期待值就会偏。

在 Native AOT 里,最终决定行不行的不是 dotnet build,而是 dotnet publish。 尽早开始跑这一步,后半程就不容易卡住。

决定行不行的是publishbuild能通过却在publish时坏掉,是因为publish时会把依赖库也包含进来认真跑分析,最终决定Native AOT行不行的不是dotnet build而是dotnet publish。dotnet build能通过行不行还没有定dotnet publish连依赖一起认真分析到这里才第一次看得出行不行所以要尽早跑publish

图15:容易踩的坑的共同点。不要因为“build 过了”就安心,要尽早跑 publish。

10. 总结

一句话概括 Native AOT,就是 把 .NET 应用从动态的执行模型,推向容易在静态阶段确定下来的分发模型的机制。

需要留意的要点,就这五条。

  1. Native AOT 在 publish 时把代码预先编译成原生代码
  2. 对启动、内存、分发方面的需求很有效
  3. 代价是对 reflection、动态代码生成、built-in COM、不支持 trimming 的代码都很严格
  4. 第一批对象比起 desktop 主体,console / worker / 小型 API 更稳
  5. 一边消掉 warning,一边以 publish 为基准尽早验证,这一点很重要

Native AOT 不是所有 .NET 应用都要打开的标配开关。 但在 启动很重要、想让分发更轻、想减少运行环境的前提条件 的场景里,它是相当强的武器。

反过来,在 WPF / WinForms / COM 味道浓的世界里,目前仍有很多场合是普通的 .NET 更合理。 能分清这两边之后,Native AOT 就不再是“难搞的新功能”,而是一个用途明确的选项。

11. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

Native AOT 是什么?
它是把 .NET 应用在 publish 时预先编译成原生代码再分发的发布方式。因为不使用运行期 JIT,启动时间和内存占用容易变好,也更容易分发到没有安装 .NET 运行时的环境。代价是与自由的反射、动态代码生成、built-in COM 以及不支持 trimming 的库都不合拍。它与其说是让程序变快的魔法,不如说是为了启动、发布、运行环境上的需要,舍弃一部分动态世界、向静态世界靠拢的发布模型。
Native AOT 和 ReadyToRun 有什么区别?
ReadyToRun 是保留 IL、把 JIT 的工作稍微提前的方式,运行期仍有会用到 JIT 的场合。它的兼容性很广,动态功能大体上也照样好用。而 Native AOT 不以运行期 JIT 本身为前提,发布产物以原生可执行文件为主,启动相当容易改善,代价是限制更强。虽然都带着 AOT 这个词,但 ReadyToRun 是“让 JIT 轻松一点”的方向,Native AOT 是“不以 JIT 为前提”的方向,力度相差很多。
WPF 或 WinForms 的应用可以用 Native AOT 吗?
在当前阶段最好相当谨慎地看待。Windows 上的 Native AOT 没有 built-in COM,WPF 与 trimming 配合不好,WinForms 对 built-in COM marshalling 的依赖很重,两者都不适合作为第一批 Native AOT 候选。如果确实需要 COM,有些场景下保持 JIT,或者以 ComWrappers / source-generated COM 为前提重新设计,反而更合理。作为入口,控制台程序、worker、小型 Web API 要自然得多。
Native AOT 适合什么样的应用?
适合以启动为主角的 CLI 工具、在容器里大量部署的小型 API、worker 与后台服务、serverless 或短生命周期进程、嵌入原生应用里的小型 .NET 组件,以及不想要求运行环境预装 .NET 运行时的场合。它们的共同点是边界相对清晰、动态机制容易减少。反过来,以运行期加载插件为主角的应用,或者强依赖用反射搜索类型的框架的结构,就不适合。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表