之前写过 把 C# 通过 Native AOT 做成原生 DLL 的方法 - 使用 UnmanagedCallersOnly 从 C/C++ 调用,但那篇其实顺序有点颠倒了。
本来应该先讲「Native AOT 到底是什么」比较自然。
Native AOT 这个话题,一开始这几点常常会混在一起。
- 是在说去掉 JIT 这件事吗
- 和 self-contained 或 single-file 有什么不同
- 和 ReadyToRun 是一类东西吗
- trimming warning 大量冒出来到底是什么情况
- WPF 或 WinForms 能不能直接套上去
这些混在一起的时候,Native AOT 看起来会像「只是变快的魔法」,或者反过来像「全是限制、不能碰的东西」。
两边都稍显粗糙。
简单说,Native AOT 是 把 .NET 应用在 publish 时相当彻底地固化下来的发布模式。
代价是对启动和发布层面效果明显。
反过来,想在运行期动态解决各种事情的程序,就不太合拍。
这篇文章就从实务角度整理这一点。
1. Native AOT 做了什么
平时的 .NET 会先生成 IL,再在运行期对需要的地方做 JIT。
Native AOT 则是把「运行期才生成原生代码」这一步大幅提前到 publish 阶段。
所以从外观上看,它是朝着「把 .NET 打包得像原生应用一样发布」的方向走。
这里重要的是 希望在 publish 时就能看到绝大多数必要的代码。
也就是说,
- 运行期才去查找类型
- 运行期才生成代码
- 运行期才读取程序集来判断
这类写法,无论如何都会比较不合适。
如果把 Native AOT 只看成「变快的功能」,这里就会理解偏了。
本质上更像是 舍弃一部分动态世界,往容易固化的静态世界靠拢。
2. 和 ReadyToRun 差在哪里
先把这里分清楚,会轻松很多。
| 角度 | 一般 JIT | ReadyToRun | Native AOT |
|---|---|---|---|
| 运行期 JIT | 使用 | 仍会使用 | 不使用 |
| 发布产物 | 以 IL 为主 | IL + 事先生成的原生代码 | 以原生可执行文件为主 |
| 启动 | 基准 | 容易改善 | 更容易改善 |
| 兼容性 | 广 | 相对广 | 限制较多 |
ReadyToRun 是把 JIT 的工作稍微提前的方向。
Native AOT 则是 不以运行期 JIT 为前提 的方向。
名字看起来接近,实际手感却差不少。
ReadyToRun 是「先让启动稍微轻一点」。
Native AOT 是「为了启动、发布、运行环境的需求,设计也往静态一点靠」。
3. 用了 Native AOT 有什么好处
最直观的还是启动速度。
在 CLI 工具、短生命周期进程、容器里的小型 API、常驻但功能有限的工具上,JIT 的开销会很明显。
Native AOT 把这部分提前做完,起步就会更轻。
另一个是发布层面。
在不想要求「请先在机器上安装 .NET Runtime」的场景中,心里会轻松不少。
- 只想发布一个小工具
- 希望容器镜像尽量小
- 不想在运行环境上装 JIT
这种场景下,Native AOT 用得上的位置很明确。
所以 Native AOT 适合的不是「所有应用」,而是 想减少启动、发布、运行环境前提条件的应用。
4. 反过来哪些地方会变辛苦
这里答案相当明确。
首先是反射与动态生成代码。
Assembly.LoadFileSystem.Reflection.Emit- 运行期把类型逐一枚举出来查找的结构
- 从字符串名称查类型再实例化的机制
这些在 publish 时不容易把必要代码固定下来,就会成为 AOT warning 的温床。
Native AOT 冒出一堆 warning 时,与其觉得「太吵了」,不如读成「这个设计在 publish 时就是看不清楚」。
尤其是 RequiresDynamicCode 系列,通常值得认真看一看。
再来是 trimming。
Native AOT 和 trimming 绑得相当紧。
这里麻烦的是,不只自己的代码,连依赖库的写法都会受影响。
例如,
- 以反射为基础的序列化器
- 以运行期扫描为前提的 DI
- 使用动态代理的库
这些都容易让事情变麻烦。
换句话说,做 Native AOT 的时候,与其「运行期再聪明处理」,不如 往编译期就能看清楚的形式靠拢 更好控制。
5. Windows 桌面端要稍微谨慎一点
在 Windows 上,Native AOT 没有内置 COM 支持。
再加上 WPF 和 trimming 的搭配并不好,WinForms 也相当依赖内置的 COM marshalling。
因此,
- 直接把 WPF / WinForms 主体丢给 Native AOT
- 照常把 COM interop 直接带进去
这些都要谨慎对待。
反过来,比较适合当入口的是,
- Console
- worker
- 小型 Web API
- 职责单一的小组件
。
Native AOT 并不是「.NET 的东西都能直接套上去」。
尤其在 Windows 桌面和 COM 味浓的世界中,很多场景还是维持一般 JIT 会更顺畅。
6. 最小步骤其实很简单
在项目端,先在 csproj 里加上 PublishAot。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
publish 的命令,例如 Windows x64 会是这样。
dotnet publish -c Release -r win-x64
Linux x64 则是,
dotnet publish -c Release -r linux-x64
走到这一步,就已经相当「像原生应用的世界」了。
由于 RID 是固定的,产物比起「同一个 DLL 到哪都能跑」,更像是为该 OS / 架构量身定制的可执行文件。
另外还有一个意外常中招的地方是 JSON。
与其用平常的方式把 System.Text.Json 写成依赖反射的路径,不如靠 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. 从哪里开始试比较好
第一个尝试对象,下列几类比较容易上手。
- 启动速度很重要的 CLI
- worker
- 小型 Web API
- 在原生集成中职责较窄的组件
这些比较容易减少动态机制,也比较容易看到 Native AOT 的好处。
反之,WPF / WinForms 既有的大型应用主体、以插件加载为核心的结构、重度依赖 COM 的结构,作为首批尝试对象并不太平顺。
8. 总结
Native AOT 是在 publish 时把 .NET 做得相当静态的机制。
- 对启动有效
- 发布也会变轻松
- 代价是对动态机制会变得严格
先掌握这三点,思路会清楚很多。
Native AOT 不是「所有 .NET 应用都该套上的标配开关」。
但在启动速度重要、想让发布更轻松、想降低对运行环境前提条件的要求时,它是相当强的选项。
反过来,在 Windows 桌面或 COM 味浓的世界中,继续走一般 .NET 往往更顺畅。
能分清楚这两边之后,Native AOT 就不再是「难搞的新功能」,而是位置清晰的工具。
9. 参考资料
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (日本語)
- Introduction to AOT warnings - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- 相关文章:把 C# 通过 Native AOT 做成原生 DLL 的方法 - 使用 UnmanagedCallersOnly 从 C/C++ 调用
- 相关文章:要在 C# 中使用原生 DLL,C++/CLI 包装是有力选项的理由 - 与 P/Invoke 的对比整理
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
Windows桌面应用的数据该存在哪里、用什么格式保存?本文整理AppData/ProgramData的使用区分,以及SQLite、JSON文件、注册表、Access(.accdb)各自的优势与陷阱,并附判断表,从实务角度讲解防止数据损坏与位数问题等注意事项。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
.NET Generic Host 到底是什么 - DI、配置、日志与 BackgroundService 的基础
本文从 DI、配置、日志、IHostedService、BackgroundService 的关系入手,整理 Generic Host 的作用,从实务角度总结它在什么地方有效、在什么地方又显得多余。
常见问题
汇总了咨询这一主题时常见的问题。
- Native AOT 是什么?
- Native AOT 是把 .NET 应用在 publish 时相当彻底地固化下来的发布模式。平时的 .NET 会先生成 IL,再在运行期对需要的地方做 JIT;Native AOT 则把「运行期才生成原生代码」这一步大幅提前到 publish 阶段,产出以原生可执行文件为主的成品,不再使用运行期 JIT。它的本质不是单纯「变快的魔法」,而是舍弃一部分动态世界,往容易固化的静态世界靠拢,对启动和发布层面的效果很明显。
- Native AOT 和 ReadyToRun 有什么区别?
- ReadyToRun 是把 JIT 的工作稍微提前的方向,发布产物是 IL 加上事先生成的原生代码,运行期仍会用到 JIT,兼容性相对广。Native AOT 则是不以运行期 JIT 为前提的方向,发布产物以原生可执行文件为主,启动更容易改善但限制更多。简单说,ReadyToRun 是「先让启动稍微轻一点」,Native AOT 是「为了启动、发布、运行环境的需求,设计也往静态靠一点」。
- 什么样的应用适合 Native AOT?
- 适合的是想减少启动、发布、运行环境前提条件的应用,例如启动速度很重要的 CLI 工具、worker、容器里的小型 Web API、职责单一的小组件。它在不想要求运行环境事先安装 .NET Runtime、希望容器镜像尽量小的场景特别有用。反过来,重度依赖反射与动态生成代码的结构、以插件加载为核心的结构,就不太合拍。
- WPF 或 WinForms 可以用 Native AOT 吗?
- 要谨慎对待。在 Windows 上 Native AOT 没有内置 COM 支持,而 WPF 和 trimming 的搭配并不好,WinForms 也相当依赖内置的 COM marshalling,所以直接把 WPF / WinForms 主体丢给 Native AOT、或者照常把 COM interop 带进去,都不建议。比较适合当入口的是 Console、worker、小型 Web API 这类职责清晰的组件;Windows 桌面和 COM 味浓的世界,很多场景维持一般 JIT 会更顺畅。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。