Native AOT 与裁剪
Native AOT 与裁剪
简介
.NET 8+ 的 Native AOT 将 C# 编译为原生机器码,无需 .NET 运行时。裁剪(Trimming)移除未使用的代码减小体积。理解 AOT 和裁剪的限制,有助于构建高性能、小体积的原生应用。Native AOT 的核心价值在于:启动时间从数百毫秒降到毫秒级、内存占用从数十 MB 降到几 MB、部署从需要整个运行时变成单个可执行文件。这对于 Serverless 函数(AWS Lambda、Azure Functions)、CLI 工具、微服务和嵌入式场景具有重大意义。但 AOT 也不是银弹——它对反射、动态代码生成和部分库的支持有严格限制,需要在项目初期就做好兼容性规划。
特点
AOT 工作原理
传统 .NET 部署 vs Native AOT
传统 .NET 应用有两种部署方式:
- 框架依赖部署(FDD):需要目标机器预装 .NET 运行时
- 自包含部署(SCD):附带整个 .NET 运行时,体积通常 60-100 MB
Native AOT 是第三种方式——在发布时将 IL 代码编译为原生机器码(如 x64 的机器指令),输出一个独立的可执行文件。它不需要 JIT 编译器,运行时直接执行机器码。
AOT 编译流程:C# 源码 -> IL 中间代码 -> RyuJIT(AOT 模式) -> 原生机器码 -> 单文件可执行文件。整个编译过程可能需要几分钟,但换来了极致的运行时性能。
实现
AOT 发布配置
<!-- 项目文件 -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<TrimMode>full</TrimMode>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<OptimizationPreference>Size</OptimizationPreference>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
</PropertyGroup>
</Project># 发布 Native AOT
dotnet publish -c Release -r win-x64
# 查看裁剪警告(非常重要!)
dotnet publish -c Release -r win-x64 /p:TreatWarningsAsErrors=true
# 指定特定 RID
dotnet publish -c Release -r linux-x64
dotnet publish -c Release -r osx-arm64AOT 兼容的 JSON 序列化
AOT 不支持运行时反射,因此传统的 JsonSerializer 会失效。需要使用 Source Generator 在编译时生成序列化代码:
using System.Text.Json.Serialization;
// 定义 Source Generator 上下文
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<User>))]
[JsonSerializable(typeof(Dictionary<string, User>))]
public partial class MyJsonContext : JsonSerializerContext { }
// 使用生成的上下文进行序列化
public record User(string Name, int Age, bool IsActive);
// 序列化
var user = new User("张三", 25, true);
var json = JsonSerializer.Serialize(user, MyJsonContext.Default.User);
Console.WriteLine(json);
// 反序列化
var deserialized = JsonSerializer.Deserialize(json, MyJsonContext.Default.User);
Console.WriteLine($"Name: {deserialized.Name}, Age: {deserialized.Age}");带选项的 JSON 序列化
// 自定义 JSON 序列化选项
[JsonSerializable(typeof(User))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class MyJsonContext : JsonSerializerContext { }
// 使用自定义选项
var options = MyJsonContext.Default.Options;
var json = JsonSerializer.Serialize(user, options);裁剪安全标记
裁剪器(Trimmer)通过静态分析确定哪些代码被使用。如果代码通过反射访问类型,裁剪器可能错误地移除这些类型。使用以下特性标记可以防止误裁剪:
using System.Diagnostics.CodeAnalysis;
// 标记整个类型不被裁剪
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(User))]
public void ProcessUser()
{
// 即使通过反射访问 User,也不会被裁剪
}
// 精细控制:只保留公共方法和属性
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.PublicProperties, typeof(User))]
public void ReflectUser()
{
var type = typeof(User);
var methods = type.GetMethods();
var props = type.GetProperties();
}
// 标记参数类型不被裁剪
public void ProcessByType(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
Type type)
{
var props = type.GetProperties(); // 裁剪安全
}
// RequiresUnreferencedCode — 标记方法使用了反射,编译时给出警告
[RequiresUnreferencedCode("此方法使用了反射,AOT 不兼容")]
public void UnsafeReflect(string typeName)
{
var type = Type.GetType(typeName)!;
Activator.CreateInstance(type);
}AOT 友好的依赖注入
// ASP.NET Core Minimal API — 天然 AOT 友好
var builder = WebApplication.CreateBuilder(args);
// 注册服务(Source Generator 会在编译时生成注册代码)
builder.Services.AddSingleton<IUserService, UserService>();
builder.Services.AddScoped<IOrderService, OrderService>();
// 使用 Source Generator 的 JSON 上下文
builder.Services.AddControllers()
.AddJsonOptions(o => o.JsonSerializerOptions.AddContext<MyJsonContext>());
var app = builder.Build();
app.MapGet("/users/{id}", (int id, IUserService service) =>
{
return service.GetUserById(id);
});
app.MapPost("/users", (User user, IUserService service) =>
{
return Results.Created($"/users/{user.Name}", service.Create(user));
});
app.Run();
public interface IUserService { User? GetUserById(int id); User Create(User user); }
public class UserService : IUserService { /* 实现 */ }AOT 不兼容的代码模式
// 以下模式在 AOT 中不兼容或需要特殊处理:
// 1. Activator.CreateInstance — 运行时创建实例
// 不兼容!需要改为直接 new 或工厂模式
// 2. Type.GetType(string) — 通过名称加载类型
// 不兼容!类型可能被裁剪
// 3. 表达式树编译 — Expression.Compile()
// 部分不兼容!简单的表达式树可以,复杂的可能失败
// 4. 动态生成 IL — Reflection.Emit
// 完全不兼容
// 5. 某些反射场景
// 需要配合 [DynamicDependency] 或 [DynamicallyAccessedMembers]
// AOT 友好的替代方案
public static class AotFriendlyFactory
{
// 使用 switch 表达式替代 Activator.CreateInstance
public static object Create(string typeName) => typeName switch
{
"User" => new User(),
"Order" => new Order(),
_ => throw new ArgumentException($"Unknown type: {typeName}")
};
}裁剪(Trimming)详解
裁剪模式
<!-- 裁剪模式配置 -->
<PropertyGroup>
<!-- full: 最大化裁剪,体积最小,但兼容性要求最高 -->
<TrimMode>full</TrimMode>
<!-- partial: 仅裁剪未引用的程序集,较安全 -->
<TrimMode>partial</TrimMode>
</PropertyGroup>- full:裁剪器会移除程序集内部未使用的类型和成员。体积最小,但需要确保所有通过反射访问的代码都有正确的标注。
- partial:只移除整个未被引用的程序集。较安全,但体积优化有限。
裁剪警告分析
# 发布时查看裁剪警告
dotnet publish -c Release -r win-x64 2>&1 | grep "IL2"
# 常见裁剪警告类型:
# IL2026: 使用了 RequiresUnreferencedCode 标记的方法
# IL2055: DynamicallyAccessedMembers 无法满足
# IL2070: 类型无法通过反射访问
# IL2075: 接口映射可能不完整
# 建议在 CI 中将裁剪警告视为错误性能对比
AOT vs JIT 性能基准
using System;
using System.Diagnostics;
// AOT 的核心优势:启动时间和内存占用
var sw = Stopwatch.StartNew();
// 模拟一个简单的计算任务
long sum = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
sum += i;
}
sw.Stop();
Console.WriteLine($"计算结果: {sum}");
Console.WriteLine($"耗时: {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"当前内存: {Environment.WorkingSet / 1024 / 1024} MB");典型对比数据(以简单的 Web API 为例):
| 指标 | JIT (SCD) | Native AOT |
|---|---|---|
| 启动时间 | 500-1500 ms | 10-50 ms |
| 内存占用 | 50-100 MB | 10-30 MB |
| 二进制体积 | 60-100 MB | 5-20 MB |
| 吞吐量 | 基准 | 基准的 80%-100% |
| 冷启动(Serverless) | 不适合 | 非常适合 |
第三方库兼容性
检查库的 AOT 兼容性
# 使用 dotnet 裁剪分析工具
dotnet publish -c Release -r win-x64 /p:PublishAot=true
# 常见 AOT 不兼容的库:
# - 旧版 Newtonsoft.Json(使用反射序列化)
# - Dapper(大量使用反射映射)
# - AutoMapper(旧版本)
# - Entity Framework Core 6 及以下
# AOT 兼容的库:
# - System.Text.Json(使用 Source Generator)
# - Dapper AOT(dotnet/efcore 的 AOT 支持)
# - EF Core 8+(部分 AOT 支持)
# - Microsoft.Extensions.* (DI, Logging, Options)编写 AOT 兼容的库
using System.Diagnostics.CodeAnalysis;
// 如果你的库可能被 AOT 项目引用,需要标注所有反射使用
public class MyLibrary
{
// 标记使用了反射的方法
[RequiresUnreferencedCode("需要通过类型名称创建实例")]
[RequiresDynamicCode("运行时代码生成")]
public static T CreateFromName<T>(string typeName) where T : class
{
var type = Type.GetType(typeName);
return (T)Activator.CreateInstance(type)!;
}
// AOT 友好的替代方案
public static T Create<T>() where T : new()
{
return new T();
}
}优点
缺点
总结
Native AOT 将 C# 编译为原生代码,启动快、体积小、内存低。需要使用 Source Generator 替代反射序列化。裁剪移除未使用代码但可能破坏反射调用。建议在 CLI 工具、微服务、Lambda 函数等对启动速度敏感的场景使用 AOT,注意检查第三方库的 AOT 兼容性。迁移到 AOT 的关键步骤:开启 PublishAot -> 修复裁剪警告 -> 用 Source Generator 替代反射 -> 性能测试验证。
关键知识点
- Native AOT 在编译时将 IL 转为原生机器码,消除了 JIT 编译的运行时开销
- 裁剪器通过静态分析移除未使用代码,但无法追踪运行时反射访问路径
- Source Generator 是 AOT 生态的核心基础设施,JSON 序列化、DI 注册都依赖它
- AOT 对比 JIT:启动时间提升 10-100 倍,吞吐量持平或略低
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 认为所有 .NET 库都能直接用于 AOT——很多库依赖反射,需要迁移
- 忽略裁剪警告——警告通常意味着运行时会崩溃
- 只在 Windows 上测试 AOT——不同平台可能有不同的兼容性问题
- 在大型遗留项目中直接启用 AOT——应该从新项目或独立模块开始
进阶路线
- 学习 Source Generator 的编写,为自己的库提供 AOT 支持
- 了解 IL 链接器(IL Linker)的工作原理和自定义裁剪配置
- 探索 Native AOT 的 PGO(Profile Guided Optimization)优化
- 研究 RyuJIT AOT 编译器的内部实现
适用场景
- Serverless 函数(AWS Lambda、Azure Functions)— 冷启动是核心痛点
- CLI 工具 — 用户期望即装即用、启动迅速
- 容器化微服务 — 更小的镜像体积、更快的启动
- 嵌入式和边缘设备 — 资源受限环境
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把 Native AOT 放进当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 项目中有多少依赖库不兼容 AOT?迁移成本如何评估?
- 在推理成本和效果之间,你的取舍标准是什么?
AOT 与依赖注入深入
// AOT 与 DI 的配合 — Source Generator 在编译时生成注册代码
// ASP.NET Core Minimal API 的 DI 注册在 AOT 下天然支持
var builder = WebApplication.CreateBuilder(args);
// 基本注册 — AOT 安全
builder.Services.AddSingleton<IUserService, UserService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailService, EmailService>();
// 工厂注册 — AOT 安全(工厂是静态可知的)
builder.Services.AddSingleton(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new RedisCacheService(config["Redis:ConnectionString"]!);
});
// 使用 AddKeyedScoped(.NET 8+)— AOT 安全
builder.Services.AddKeyedScoped<IRepository, UserRepository>("primary");
builder.Services.AddKeyedScoped<IRepository, ReadOnlyRepository>("readonly");
// TryAdd 模式 — AOT 安全
builder.Services.TryAddSingleton<ICacheService, RedisCacheService>();
// AOT 不安全的 DI 模式:
// 1. 基于名称的动态注册
// 2. 通过字符串创建类型实例
// 3. 使用 Scrutor 等基于反射的程序集扫描库
// 解决方案:使用 Source Generator 或显式注册
// 例如使用 Scrutor 的替代方案 — 手动注册所有服务
builder.Services.Scan(scan => scan
.FromAssemblyOf<UserService>()
.AddClasses(classes => classes.AssignableTo<IService>())
.AsMatchingInterface()
.WithSingletonLifetime()
);
// 注意:Scrutor 的 Scan 模式在 AOT 下可能有问题,
// 建议使用显式注册或 Scrutor 的 AOT 兼容版本AOT 与配置管理
// AOT 环境下的配置管理
// 1. Options 模式 — AOT 安全(使用 Source Generator)
builder.Services.Configure<AppOptions>(
builder.Configuration.GetSection("App")
);
public class AppOptions
{
public string ConnectionString { get; set; } = "";
public int MaxRetryCount { get; set; } = 3;
public string[] AllowedOrigins { get; set; } = [];
}
// 2. 强类型配置绑定 — AOT 安全
var appOptions = new AppOptions();
builder.Configuration.GetSection("App").Bind(appOptions);
// 3. 自定义 Source Generator for Options
// .NET 8+ 可以使用 JsonSchemaExporter 等 Source Generator
// 4. 配置文件热更新 — AOT 安全
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
// 5. 验证 Options
builder.Services.AddOptions<AppOptions>()
.Bind(builder.Configuration.GetSection("App"))
.ValidateDataAnnotations()
.Validate(options =>
{
if (string.IsNullOrEmpty(options.ConnectionString))
throw new InvalidOperationException("ConnectionString is required");
return true;
});AOT 调试技巧
// AOT 调试方法
// 1. 检查裁剪警告
// dotnet publish -c Release -r win-x64 /p:TreatWarningsAsErrors=true
// 常见警告:
// IL2026: RequiresUnreferencedCode
// IL2055: DynamicallyAccessedMembers 不满足
// IL2070: 类型通过反射访问
// IL2087: 类型无法实例化
// 2. 使用 IL 查看 AOT 编译结果
// dotnet publish -c Release -r win-x64 /p:PublishAot=true
// 检查发布目录中的文件
// 3. 裁剪警告的详细分析
// 启用详细日志:
// dotnet publish -c Release -r win-x64 -v:detailed
// 4. 使用 TrimmerRootDescriptor
// 创建一个 XML 文件标记不希望被裁剪的程序集和类型
// linker-descriptor.xml:
/*
<linker>
<assembly fullname="MyLibrary" preserve="all" />
<assembly fullname="MyLibrary">
<type fullname="MyLibrary.Models.User" preserve="all" />
</assembly>
</linker>
*/
// 在项目文件中引用:
// <ItemGroup>
// <TrimmerRootDescriptor Include="linker-descriptor.xml" />
// </ItemGroup>
// 5. AOT 编译性能
// AOT 编译通常比普通编译慢 5-20 倍
// 建议:
// - CI 中使用增量构建和缓存
// - 开发阶段不用 AOT,只在发布时启用
// - 使用 -p:PublishReadyToRun=true 预编译AOT 实战:构建 CLI 工具
// Program.cs — AOT 友好的 CLI 工具
using System.CommandLine;
var rootCommand = new RootCommand("文件处理工具");
var inputFile = new Option<FileInfo>(
"--input", "输入文件路径") { IsRequired = true };
var outputFile = new Option<FileInfo>(
"--output", "输出文件路径") { IsRequired = true };
var format = new Option<string>(
"--format", () => "json", "输出格式 (json/csv)");
var convertCommand = new Command("convert", "转换文件格式");
convertCommand.AddOption(inputFile);
convertCommand.AddOption(outputFile);
convertCommand.AddOption(format);
convertCommand.SetHandler(ConvertFile, inputFile, outputFile, format);
rootCommand.AddCommand(convertCommand);
return await rootCommand.InvokeAsync(args);
// AOT 安全的文件转换逻辑
void ConvertFile(FileInfo input, FileInfo output, string format)
{
if (!input.Exists)
{
Console.Error.WriteLine($"文件不存在: {input.FullName}");
Environment.Exit(1);
}
var content = File.ReadAllText(input.FullName);
// 使用 AOT 安全的 JSON 序列化
string result = format switch
{
"json" => ConvertToJson(content),
"csv" => ConvertToCsv(content),
_ => throw new ArgumentException($"不支持的格式: {format}")
};
File.WriteAllText(output.FullName, result);
Console.WriteLine($"转换完成: {output.FullName}");
}
// AOT 安全的 JSON 转换
string ConvertToJson(string content)
{
// 使用 Source Generator 生成的序列化代码
return content; // 简化示例
}
string ConvertToCsv(string content)
{
return content; // 简化示例
}
// 项目文件
/*
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>
*/AOT 迁移清单:
- [ ] 检查所有 NuGet 包的 AOT 兼容性
- [ ] 用 Source Generator 替换 JsonSerializer 的反射用法
- [ ] 将 Activator.CreateInstance 替换为工厂模式
- [ ] 标记所有 [RequiresUnreferencedCode] 和 [RequiresDynamicCode]
- [ ] 修复所有裁剪警告(IL2xxx)
- [ ] 在所有目标平台测试
- [ ] 对比 AOT 前后的性能和体积
- [ ] 更新 CI/CD 流程(AOT 编译时间更长)