支持 NativeAOT 或 Trimming 的库是什么原理?如果自已写一个简单的.NET类库,如何实现支持别人下载使用时可以AOT编绎
好的,这是一个非常棒的问题,涉及到 .NET 生态发展的核心。下面我将分两部分详细解释:一是支持 AOT/剪裁的库的原理,二是如何自己编写一个支持 AOT 的类库。
第一部分:支持 NativeAOT 或 Trimming 的库是什么原理?
核心原理是:通过静态分析,为剪裁器和 AOT 编译器提供足够的元数据,以确保动态代码行为在剪裁后依然正确。
传统的 .NET 程序集包含大量元数据,运行时(JIT)可以通过反射动态发现和调用任何成员。但 AOT 编译和剪裁是静态过程,它必须提前知道应用程序最终会用到哪些代码,从而将"用不到的代码抛弃"。
问题就出在"动态"上。如果一个库大量使用反射、动态加载、dynamic
关键字或 System.Reflection.Emit
,剪裁器很难通过静态分析判断哪些类型/成员会被使用到,从而导致剪裁过度,运行时出错。
支持 AOT/剪裁的库通过以下关键技术来实现兼容:
1. 使用 [DynamicallyAccessedMembers]
属性进行注解(最重要!)
这是向剪裁器传递意图的核心手段。这个属性用来修饰参数、字段或返回值,告诉剪裁器:"这个 Type
/string
虽然现在看起来是动态的,但它最终会访问哪些种类的成员"。
例子对比:
-
不兼容 AOT 的代码:
cs// 剪裁器不知道 `typeName` 这个字符串代表什么,更不知道需要保留它的哪些成员。 // 剪裁后,如果 `SomeMethod` 被剪掉了,这里运行时就会抛出 MissingMethodException。 public object CreateAndCallMethod(string typeName, string methodName) { Type type = Type.GetType(typeName); object instance = Activator.CreateInstance(type); return type.GetMethod(methodName).Invoke(instance, null); }
-
兼容 AOT 的代码:
cs// 告诉剪裁器:`typeName` 参数代表的类型本身及其所有公共方法都需要被保留,不能被剪掉。 public object CreateAndCallMethod( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] string typeName, string methodName) { Type type = Type.GetType(typeName); object instance = Activator.CreateInstance(type); return type.GetMethod(methodName).Invoke(instance, null); }
使用了
DynamicallyAccessedMemberTypes.PublicMethods
后,剪裁器会分析所有传入typeName
参数的可能值,并确保这些类型的所有公共方法都被保留下来,从而避免运行时错误。
2. 使用源生成器(Source Generators)替代反射
这是更现代、更安全的方法。源生成器在编译时就根据某些约定或注解生成出静态代码,完全避免了运行时反射。
- 例子 :JSON 序列化库(如
System.Text.Json
)用源生成器为已知的模型类型生成高度优化的序列化/反序列化代码,而不是在运行时通过反射来读取模型属性。
3. 提供 RD.XML 文件(备选方案)
有时无法通过代码注解完全描述所有动态行为。库作者可以提供一个可选的 rd.xml
文件,用户可以在其应用中引用这个文件。该文件使用 XML 语法显式地指令剪裁器/AOT 编译器保留特定的程序集、类型、方法等。
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<!-- 告诉编译器:保留 `MySpecialType` 的所有成员,即使用户代码没显式用到 -->
<Type Name="MyLibrary.MySpecialType" Dynamic="Required All" />
</Application>
</Directives>
4. 避免或明确标记不兼容的模式
库作者应尽量避免使用根本无法静态分析的模式,如:
-
Assembly.LoadFrom(someDynamicString)
-
大量使用
dynamic
-
复杂的
Reflection.Emit
如果无法避免,必须在文档中明确说明该库不支持剪裁/AOT ,或者需要用户提供复杂的 rd.xml
配置。
第二部分:如何自己编写一个支持 AOT 的简单类库
假设你要创建一个简单的工具库 MyAotFriendlyLib
。
第 1 步:创建类库项目
使用 .NET 8 或更高版本的 SDK 创建项目。.csproj
文件是现代 SDK 风格。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- 这是一个重要的开关,表示你承诺该库支持剪裁 -->
<IsTrimmable>true</IsTrimmable>
<!-- 启用剪裁分析,这样编译器就会帮你找出潜在问题 -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<!-- 如果你明确只支持AOT,可以启用AOT分析(.NET 8+) -->
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>
</Project>
第 2 步:编写代码时遵循 AOT 原则
-
尽可能使用静态代码。
-
必须使用反射时,用
[DynamicallyAccessedMembers]
精细注解。 -
考虑使用源生成器来替代复杂的反射逻辑。
示例代码 (TextFormatter.cs
):
cs
using System.Diagnostics.CodeAnalysis;
namespace MyAotFriendlyLib
{
public static class TextFormatter
{
// 一个普通的静态方法,完全AOT安全
public static string FormatHello(string name) {
return $"Hello, {name}!";
}
// 一个使用了反射,但通过注解保证AOT安全的方法
public static string? GetTypeName(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
Type type)
{
// 因为我们注解了"公共构造函数",剪裁器会保证它们不被剪掉。
// 所以这行`GetConstructors`调用是安全的。
var constructors = type.GetConstructors();
return type.FullName;
}
}
}
第 3 步:在本地测试剪裁和 AOT
创建一个控制台应用来引用和测试你的库。
测试项目的 .csproj
:
cs
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<!-- 启用发布时剪裁 -->
<PublishTrimmed>true</PublishTrimmed>
<!-- 如果你要测试NativeAOT发布 -->
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<!-- 引用你的本地库项目,而不是NuGet包 -->
<ProjectReference Include="..\MyAotFriendlyLib\MyAotFriendlyLib.csproj" />
</ItemGroup>
</Project>
运行 dotnet publish -c Release -r win-x64
(或其他 RID)来测试发布和剪裁/AOT 是否成功。务必运行生成的可执行文件进行测试!
第 4 步:打包并上传到 NuGet
当你确认库工作正常后,就像发布普通 NuGet 包一样打包它。
dotnet pack -c Release nuget push .\MyAotFriendlyLib.1.0.0.nupkg -Source https://api.nuget.org/v3/index.json
用户如何识别和使用:
用户在你的 NuGet 包页面上会看到 <IsTrimmable>true</IsTrimmable>
的元数据,他们就会知道这个库可以在他们进行 PublishTrimmed
或 PublishAot
时安全使用。
总结
操作 | 原理 | 实践 |
---|---|---|
静态分析 | 编译器提前分析代码依赖 | 启用 <EnableTrimAnalyzer>true</EnableTrimAnalyzer> |
注解意图 | 用 [DynamicallyAccessedMembers] 指导剪裁器 |
修饰反射代码的参数、字段等 |
生成静态代码 | 用源生成器在编译时取代运行时反射 | 为高性能和AOT安全场景设计 |
显式指令 | 通过 rd.xml 文件硬性保留成员 |
作为最后的手段,并提供给用户 |
通过以上步骤和原则,你就能创建出高质量的、支持 NativeAOT 和剪裁的现代 .NET 库,为整个生态的进步做出贡献。