名称: dotnet-maui-aot 描述: “为 iOS/Catalyst 优化 MAUI。本地 AOT 管道、大小/启动增益、库间隙、修剪。” 用户不可调用: false
dotnet-maui-aot
在 iOS 和 Mac Catalyst 上为 .NET MAUI 进行本地 AOT 编译:编译管道、发布配置文件、最多 50% 应用大小减少和最多 50% 启动改进、库兼容性间隙、退出机制、修剪交互(RD.xml、源生成器),以及在设备上测试 AOT 构建。
版本假设: .NET 8.0+ 基线。MAUI 的本地 AOT 在 iOS 和 Mac Catalyst 上可用。Android 使用不同的编译模型(.NET 11 中的 CoreCLR,.NET 8-10 中的 Mono/AOT)。
范围
- iOS/Mac Catalyst AOT 编译管道
- MAUI AOT 的发布配置文件配置
- 大小/启动改进测量
- MAUI AOT 应用程序的库兼容性间隙
- 退出机制和修剪交互
- 在设备上测试 AOT 构建
超出范围
- MAUI 开发模式(项目结构、XAML、MVVM)——见[技能:dotnet-maui-development]
- MAUI 测试——见[技能:dotnet-maui-testing]
- WASM AOT(Blazor/Uno)——见[技能:dotnet-aot-wasm]
- 通用 AOT 架构——见[技能:dotnet-native-aot]
交叉引用:[技能:dotnet-maui-development] 用于 MAUI 模式,[技能:dotnet-maui-testing] 用于测试 AOT 构建,[技能:dotnet-native-aot] 用于通用 AOT 模式,[技能:dotnet-aot-wasm] 用于 WASM AOT,[技能:dotnet-ui-chooser] 用于框架选择。
iOS/Mac Catalyst AOT 编译管道
在 iOS 和 Mac Catalyst 上的本地 AOT 在发布时将 .NET IL 直接编译为本机机器代码,消除了运行时对 JIT 编译器或 IL 解释器的需求。这产生了一个链接到平台框架的自包含本机二进制文件。
工作原理
- IL 编译: .NET IL 由 NativeAOT 编译器(ILC)编译为本机代码
- 树摇: 未使用的代码基于可达类型和方法的静态分析被修剪
- 本机链接: 生成的本机代码与 iOS/Catalyst 框架和最小 .NET 运行时链接
- 应用包: 结果是一个标准 .app 包,带有本机可执行文件(不包含 IL 程序集)
发布配置
<!-- 为 iOS/Mac Catalyst 启用本地 AOT -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0-ios' Or
'$(TargetFramework)' == 'net8.0-maccatalyst'">
<PublishAot>true</PublishAot>
<!-- 可选:去除调试符号以减小二进制大小 -->
<StripSymbols>true</StripSymbols>
</PropertyGroup>
# 为 iOS 发布 AOT
dotnet publish -f net8.0-ios -c Release -r ios-arm64
# 为 Mac Catalyst 发布 AOT
dotnet publish -f net8.0-maccatalyst -c Release -r maccatalyst-arm64
# 为 iOS 模拟器发布(用于无需设备的 AOT 测试)
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64
权利和配置文件
AOT 构建需要与常规 iOS/Catalyst 构建相同的权利和配置文件。AOT 本身不需要特殊权利。
<!-- iOS 权利 (Entitlements.plist) -->
<!-- 标准权利;AOT 不需要特殊条目 -->
大小减少
本地 AOT 在 iOS 上可以实现最多 50% 的应用大小减少,相比解释器/JIT 模式。大小改进来自:
- 树摇: 只有可达代码包含在最终二进制文件中
- 无 IL 运送: 应用包不包含 .NET IL 程序集
- 无运行时 JIT: JIT 编译器及相关元数据不打包
典型大小比较
| 模式 | 近似大小 | 说明 |
|---|---|---|
| 解释器(默认 .NET 8 iOS) | ~60-80 MB | 包括 IL 程序集 + 解释器 |
| 本地 AOT | ~30-45 MB | 仅本机二进制文件,无 IL |
| 本地 AOT + 去除符号 | ~25-40 MB | 调试符号已去除 |
注意事项: 实际大小减少取决于应用复杂性、第三方库使用情况,以及修剪后可达代码量。使用重度反射的库可能阻止激进修剪并减少大小收益。
启动改进
本地 AOT 在 iOS 和 Mac Catalyst 上提供最多 50% 更快的冷启动。启动改进来自:
- 无 JIT 预热: 代码已是本机;应用启动时无编译
- 无 IL 加载: 无需加载和解析 .NET 程序集
- 减少内存压力: 启动期间工作集更小
测量启动
// 仪器启动计时
public partial class App : Application
{
private static readonly long StartTicks = Stopwatch.GetTimestamp();
public App()
{
InitializeComponent();
MainPage = new AppShell();
var elapsed = Stopwatch.GetElapsedTime(StartTicks);
System.Diagnostics.Debug.WriteLine(
$"应用启动: {elapsed.TotalMilliseconds:F0}ms");
}
}
# 使用 Xcode Instruments 进行精确启动测量
# Time Profiler 模板 → 测量 "pre-main" + "post-main" 时间
# 在同一设备上比较 AOT 与非 AOT 构建
库兼容性
许多 .NET 库不 fully AOT-compatible。常见兼容性问题源于:
- 反射: 运行时类型检查,
Type.GetType(),Activator.CreateInstance() - 动态代码生成:
System.Reflection.Emit,System.Linq.Expressions.Compile() - 无源生成器的序列化: 使用反射的 JSON/XML 序列化器
兼容性矩阵
| 库 / 功能 | AOT 状态 | 解决方案 |
|---|---|---|
| System.Text.Json(源生成) | 兼容 | 使用 [JsonSerializable] 上下文 |
| System.Text.Json(反射) | 不兼容 | 切换到源生成器 |
| CommunityToolkit.Mvvm | 兼容 | 基于源生成,AOT 安全 |
| Entity Framework Core | 部分兼容 | 预编译查询;避免动态 LINQ |
| Newtonsoft.Json | 不兼容 | 迁移到带源生成的 System.Text.Json |
| AutoMapper | 不兼容 | 使用 Mapperly(源生成) |
| MediatR | 部分兼容 | 显式注册处理程序,避免程序集扫描 |
| HttpClient | 兼容 | 标准用法有效 |
| MAUI Essentials | 兼容 | 平台 API 是 AOT 安全 |
| SQLite-net | 兼容 | 使用 P/Invoke,AOT 安全 |
| Refit | 不兼容 | 使用 Refit 7+(包括源生成器;使用 [GenerateRefitClient] 启用) |
| FluentValidation | 部分兼容 | 避免运行时表达式编译 |
检测不兼容代码
<!-- 在开发期间启用 AOT 分析警告 -->
<PropertyGroup>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<!-- 同时启用修剪分析器(AOT 需要修剪) -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
AOT 分析产生警告如 IL3050(RequiresDynamicCode)和 IL2026(RequiresUnreferencedCode)。在发布 AOT 前解决这些。
退出机制
完全禁用 AOT
<!-- 禁用本地 AOT(使用解释器/JIT 模式) -->
<PropertyGroup>
<PublishAot>false</PublishAot>
</PropertyGroup>
按程序集修剪覆盖
当特定库不 AOT 兼容时,可以保留它不被修剪,同时其他部分使用 AOT:
<!-- 保留特定程序集免修剪 -->
<ItemGroup>
<TrimmerRootAssembly Include="IncompatibleLibrary" />
</ItemGroup>
退出 .NET 11 默认值
.NET 11 引入与 AOT 交互的新默认值:
<!-- 恢复 XAML 源生成(使用遗留 XAMLC) -->
<PropertyGroup>
<MauiXamlInflator>XamlC</MauiXamlInflator>
</PropertyGroup>
<!-- 恢复 Android 上的 Mono 运行时(与 iOS AOT 无关,但影响整体 MAUI AOT 故事) -->
<PropertyGroup>
<UseMonoRuntime>true</UseMonoRuntime>
</PropertyGroup>
修剪交互
本地 AOT 需要修剪。当 PublishAot 为 true 时,修剪自动启用。理解修剪配置对成功 AOT 构建至关重要。
用于反射保留的 ILLink 描述符
注意: 在 Xamarin/Mono 时代文档中,这些称为 “rd.xml”(运行时指令)。在 .NET 8+ 本地 AOT 中,使用 ILLink 描述符 XML 文件代替。
当代码使用修剪器无法静态分析的反射时,使用 ILLink 描述符 XML 文件保留类型。也可以在代码中使用 [DynamicDependency] 属性进行细粒度保留。
ILLink 描述符 XML(优先用于批量保留):
<!-- ILLink.Descriptors.xml -- 保留运行时需要的类型 -->
<linker>
<!-- 保留类型的所有公共成员 -->
<assembly fullname="MyApp">
<type fullname="MyApp.Models.LegacyConfig" preserve="all" />
<type fullname="MyApp.Services.PluginLoader">
<method name="LoadPlugin" />
</type>
</assembly>
<!-- 保留外部程序集中的所有类型 -->
<assembly fullname="IncompatibleLibrary" preserve="all" />
</linker>
<!-- 在 .csproj 中注册描述符 -->
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
[DynamicDependency] 属性(优先用于定向保留):
using System.Diagnostics.CodeAnalysis;
// 保留类型上的特定方法
[DynamicDependency(nameof(LegacyConfig.Initialize), typeof(LegacyConfig))]
public void ConfigureApp() { /* ... */ }
// 保留类型的所有公共成员
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(LegacyConfig))]
public void LoadPlugins() { /* ... */ }
源生成器替代方案
当源生成器不可用时,使用 [DynamicDependency] 属性(如上所示)进行定向保留,而无需 ILLink XML 文件。
优先使用源生成器而非反射,完全避免修剪问题:
| 反射模式 | 源生成器替代方案 |
|---|---|
JsonSerializer.Deserialize<T>() |
[JsonSerializable] 上下文(System.Text.Json) |
Activator.CreateInstance<T>() |
显式注册的工厂模式 |
Type.GetProperties() |
CommunityToolkit.Mvvm [ObservableProperty] |
| 程序集扫描用于 DI | 显式 services.Add*() 注册 |
| AutoMapper 反射映射 | Mapperly [Mapper] 源生成器 |
修剪警告
# 构建带详细修剪警告
dotnet publish -f net8.0-ios -c Release /p:PublishAot=true /p:TrimmerSingleWarn=false
# TrimmerSingleWarn=false 显示每次出现警告而非每个程序集一个总结警告,便于修复问题
常见修剪警告:
- IL2026: 带有
RequiresUnreferencedCode的成员——成员执行修剪后不能保证工作的操作 - IL2046: 基/派生类型间的修剪属性不匹配
- IL3050: 带有
RequiresDynamicCode的成员——成员在运行时生成代码(与 AOT 不兼容)
测试 AOT 构建
AOT 构建的行为可能与 Debug/JIT 构建不同。发布前总是在真实设备或模拟器上使用 AOT 发布构建进行测试。
常见 AOT 特有故障
| 故障 | 症状 | 修复 |
|---|---|---|
| 缺少类型元数据 | 运行时 MissingMetadataException |
将类型添加到 ILLink 描述符或使用 [DynamicDependency] |
| 修剪方法 | MissingMethodException |
添加 [DynamicDependency] 或 ILLink 描述符条目 |
| 动态代码生成 | PlatformNotSupportedException |
替换为源生成器替代方案 |
| 基于反射的序列化 | 反序列化对象为空/null | 使用 [JsonSerializable] 源生成 |
| 程序集扫描 | 运行时缺少服务 | 在 DI 中显式注册服务 |
测试工作流
# 1. 构建并发布 AOT 到模拟器(更快迭代)
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64
# 2. 在模拟器上安装和测试
#(使用 Xcode 或 Visual Studio 将 .app 部署到模拟器)
# 3. 运行冒烟测试——重点关注:
# - 应用启动(无 MissingMetadataException)
# - JSON 反序列化(所有属性填充)
# - 导航(所有页面渲染)
# - 平台服务(生物识别、摄像头、位置)
# - 第三方 SDK 集成
# 4. 发布前在物理设备上测试
dotnet publish -f net8.0-ios -c Release -r ios-arm64
# 通过 Xcode 和配置文件部署
CI 集成
# CI 管道:构建 AOT 并通过 XHarness 运行设备测试
dotnet publish -f net8.0-ios -c Release -r iossimulator-arm64 /p:PublishAot=true
xharness apple test \
--app bin/Release/net8.0-ios/iossimulator-arm64/publish/MyApp.app \
--target ios-simulator-64 \
--timeout 00:10:00 \
--output-directory test-results/aot
对于 MAUI 测试模式(Appium、XHarness),见[技能:dotnet-maui-testing]。
代理注意事项
- 不要启用
PublishAot而不启用修剪分析器。 AOT 需要修剪。设置<EnableTrimAnalyzer>true</EnableTrimAnalyzer>和<EnableAotAnalyzer>true</EnableAotAnalyzer>在开发期间及早发现问题。 - 不要假设所有 NuGet 包都 AOT 兼容。 检查包 .csproj 中的
IsAotCompatible或在构建时查找修剪/AOT 警告。许多流行包仍内部使用反射。 - 不要使用
Newtonsoft.Json与 AOT。 它完全依赖反射。迁移到带[JsonSerializable]源生成上下文的System.Text.Json以实现 AOT 安全序列化。 - 不要跳过 AOT 构建的设备测试。 模拟器测试捕获大多数问题,但物理设备行为可能不同——特别是启动计时、内存约束和平台服务集成。
- 不要混淆 MAUI iOS AOT 与 Android AOT。 MAUI 本地 AOT(
PublishAot)仅针对 iOS 和 Mac Catalyst。Android 使用不同的编译模型(.NET 8-10 中的 Mono AOT,.NET 11+ 中的 CoreCLR)。它们是分开配置的。
先决条件
- .NET 8.0+ 带 MAUI 工作负载
- Xcode 和 iOS/Mac Catalyst SDK(仅 macOS)
- Apple 开发者账户用于物理设备部署
- 设备测试的配置文件和签名证书