前言

最近我使用 Avalonia 开发了一个文章发布工具,StarBlog Publisher

Avalonia 是一个跨平台的 UI 框架,它可以在 Windows、Linux 和 macOS 上运行。它的特点是高性能、跨平台、易于使用。

Avalonia 有很多优点,比如高性能、跨平台、易于使用。但是,它也有一些缺点,比如学习曲线较陡峭、文档较难找到。

但 Avalonia 是基于 .NetCore 框架开发的,最终打包出来的可执行文件,如果选择 framework-dependant 发布,那么需要在客户端上安装 .NetCore 运行时环境,这对用户来说是一个很大的负担。如果使用 self-contained 发布,体积又比较大。

并且还容易被反编译,这在一些商业软件中是不允许的。(不过我这个项目是开源的,所以没有这个问题)

本文以 StarBlog Publisher 项目为例,记录一下使用 AOT 发布 Avalonia 应用的踩坑过程。

新的1.1版本已经发布,欢迎下载尝试: https://github.com/star-blog/starblog-publisher/releases

关于 AOT

从 .Net7 开始,逐步开始支持 AOT 发布,这是一个非常重要的特性。AOT 发布可以将 .Net 应用程序编译成不依赖运行库的机器码,体积较小,而且不容易被反编译。

AOT 发布的原理是将.Net 应用程序编译成 LLVM IR 代码,然后使用 LLVM 编译器将 LLVM IR 代码编译成机器码。LLVM 编译器可以将 LLVM IR 代码编译成不同的目标平台的机器码。

目前的 LTS 版本是 .Net8,对 AOT 的支持已经比较完善了,这次我来尝试使用 AOT 方式发布 Avalonia 应用。

PS:据说 .Net9 对 AOT 方式提供了很多优化和改进,接下来我会尝试一下。

使用 AOT 可能会遇到的问题

  • 兼容性问题 :AOT编译可能与某些依赖库不兼容,特别是那些依赖反射、动态代码生成或JIT编译的库。如果遇到问题,可能需要在rd.xml中添加更多配置。
  • 包大小 :AOT编译会生成更大的可执行文件(相比起 framework-dependant 模式而言),但启动速度更快。
  • 调试困难 :AOT编译的应用程序调试可能更加困难。
  • 第三方库 :检查项目中使用的第三方库是否支持AOT编译。例如, Microsoft.Extensions.AI 和 Microsoft.Extensions.AI.OpenAI 是预览版,可能需要特别注意其AOT兼容性。
  • Avalonia特定配置 :对于Avalonia应用,可能需要确保XAML相关的类型信息被正确保留。

修改项目文件

首先需要在项目文件中添加AOT相关的配置:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <!-- AOT 相关配置 -->
<PublishAot>true</PublishAot>
<TrimMode>full</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies> <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup> <!-- 其余部分保持不变 -->
</Project>

JSON序列化问题

在AOT编译环境中,JSON序列化是一个常见的问题点,因为它通常依赖于运行时反射。

这个项目有几个地方用到了 JSON

一个是应用设置,另一个是网络请求

先说结论:Newtonsoft.Json 相比 System.Text.Json 对 AOT 的支持更好,如果要使用 AOT,优先使用 Newtonsoft.Json 库。

修改应用设置 AppSettings.cs 支持AOT

如果非要使用 System.Text.Json ,那么需要修改一下。用 Newtonsoft.Json 的话直接跳过。

using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using StarBlogPublisher.Services.Security;
using System.Text.Json.Serialization.Metadata; // 添加此命名空间 namespace StarBlogPublisher.Services; // 添加JsonSerializable特性,为AOT生成序列化代码
[JsonSerializable(typeof(AppSettings))]
internal partial class AppSettingsContext : JsonSerializerContext
{
} public class AppSettings {
private static readonly string ConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StarBlogPublisher",
"settings.json"
); // ... 现有代码 ... private static AppSettings Load() {
try {
if (File.Exists(ConfigPath)) {
var json = File.ReadAllText(ConfigPath);
// 使用AOT友好的序列化方式
var settings = JsonSerializer.Deserialize(json, AppSettingsContext.Default.AppSettings);
return settings ?? new AppSettings();
}
}
catch (Exception ex) {
// 如果加载失败,返回默认设置
Console.WriteLine($"Failed to load app settings. {ex}");
} return new AppSettings();
} public void Save() {
try {
var directory = Path.GetDirectoryName(ConfigPath);
if (!string.IsNullOrEmpty(directory)) {
Directory.CreateDirectory(directory);
} // 使用AOT友好的序列化方式
var json = JsonSerializer.Serialize(this, AppSettingsContext.Default.AppSettings, new JsonSerializerOptions {
WriteIndented = true
});
File.WriteAllText(ConfigPath, json); // 触发配置变更事件
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
catch (Exception) {
// todo 处理保存失败的情况
}
}
}

解释

  1. 添加了 [JsonSerializable] 特性和 JsonSerializerContext 派生类,这是.NET中支持AOT的JSON序列化的关键。这会在编译时生成序列化代码,而不是依赖运行时反射。
  2. 修改了 Load()Save() 方法,使用 AppSettingsContext.Default.AppSettings 作为类型信息,而不是依赖运行时类型推断。
  3. 这种方法确保了在AOT环境中,所有需要的序列化代码都会在编译时生成,而不需要运行时反射。

此外,还需要在项目文件中确保已启用AOT编译的JSON源生成器:

<PropertyGroup>
<!-- 其他属性 -->
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>

这些修改将确保AppSettings类在AOT编译环境中能够正确地进行JSON序列化和反序列化。

Refit在AOT模式下的JSON序列化问题

在AOT模式下,Refit库的JSON处理也可以使用 Newtonsoft.Json

先安装 Refit.Newtonsoft.Json 库,并且需要额外配置来处理类型信息。

添加类型预注册

需要创建一个新的类来预注册所有API接口中使用的类型:

using Newtonsoft.Json;
using StarBlogPublisher.Models;
using System.Collections.Generic;
using CodeLab.Share.ViewModels.Response; namespace StarBlogPublisher.Services; /// <summary>
/// 为AOT编译预注册Refit使用的类型
/// </summary>
public static class RefitTypeRegistration
{
/// <summary>
/// 在应用启动时调用此方法,确保所有类型都被预注册
/// </summary>
public static void RegisterTypes()
{
// 注册常用的响应类型
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
// 添加自定义转换器如果需要
Converters = new List<JsonConverter>
{
// 可以添加自定义转换器
}
}; // 预热类型 - 确保这些类型在AOT编译时被包含
var types = new[]
{
typeof(ApiResponse<>),
typeof(ApiResponse<List<Category>>),
typeof(ApiResponse<List<WordCloud>>),
// 添加其他API响应类型
typeof(List<Category>),
typeof(Category),
typeof(WordCloud),
// 添加所有模型类型
}; // 触发类型加载
foreach (var type in types)
{
var _ = type.FullName;
}
}
}

修改ApiService类

修改ApiService类,确保在初始化时注册类型:

using Refit;
using StarBlogPublisher.Services.StarBlogApi;
using System;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json; namespace StarBlogPublisher.Services; public class ApiService {
private static ApiService? _instance; public static ApiService Instance {
get {
_instance ??= new ApiService();
return _instance;
}
} private readonly RefitSettings _refitSettings; private ApiService() {
// 确保类型被注册
RefitTypeRegistration.RegisterTypes(); // 配置Refit设置
_refitSettings = new RefitSettings(new NewtonsoftJsonContentSerializer(
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
// 禁用反射优化,这在AOT环境中很重要
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
}
));
} // ... 其余代码保持不变 ...
}

在App.axaml.cs中初始化类型注册

确保在应用启动时调用类型注册:

public override void OnFrameworkInitializationCompleted()
{
// 确保Refit类型被注册
RefitTypeRegistration.RegisterTypes(); // 其他初始化代码... base.OnFrameworkInitializationCompleted();
}

添加AOT兼容性配置

由于AOT编译对反射和动态代码生成有限制,需要添加一个rd.xml文件来指定需要保留的类型:

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<!-- 添加需要在AOT中保留的程序集和类型 -->
<Assembly Name="StarBlogPublisher" Dynamic="Required All" />
<Assembly Name="Avalonia.Markup.Xaml" Dynamic="Required All" />
<Assembly Name="Avalonia" Dynamic="Required All" /> <!-- Refit相关程序集 -->
<Assembly Name="Refit" Dynamic="Required All" />
<Assembly Name="Newtonsoft.Json" Dynamic="Required All" /> <!-- 添加API接口和模型类型 -->
<Assembly Name="StarBlogPublisher">
<Type Name="StarBlogPublisher.Services.StarBlogApi.IAuth" Dynamic="Required All" />
<Type Name="StarBlogPublisher.Services.StarBlogApi.ICategory" Dynamic="Required All" />
<Type Name="StarBlogPublisher.Services.StarBlogApi.IBlogPost" Dynamic="Required All" />
</Assembly> <Assembly Name="CodeLab.Share">
<Type Name="CodeLab.Share.ViewModels.Response.ApiResponse`1" Dynamic="Required All" />
</Assembly>
</Application>
</Directives>

然后在项目文件中引用这个rd.xml文件:

<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>

发布

使用以下命令发布AOT版本的应用程序:

dotnet publish -c Release -r win-x64 -p:PublishAot=true

对于其他平台,可以替换相应的RID:

  • Windows: win-x64
  • macOS: osx-x64
  • Linux: linux-x64

小结

AOT 发布是 .Net 平台一个重要的特性,它能将应用程序编译成不依赖运行时的机器码,不仅减小了发布包体积,还能提升启动速度,同时也增加了反编译的难度。

使用 AOT 方式发布 Avalonia 应用程序还是有一些坑的。:JSON序列化问题、类型注册问题以及AOT兼容性问题。针对这些问题,以下解决方案可以解决:

  1. JSON序列化方面,优先选择了对AOT支持更好的 Newtonsoft.Json 库,并通过类型预注册确保了序列化的正确性。
  2. 对于需要反射的功能,通过rd.xml文件显式声明需要保留的类型,解决了AOT编译时的类型裁剪问题。
  3. 在项目配置方面,通过合理设置AOT相关的编译选项,平衡了性能和包大小。

虽然AOT发布还存在一些限制,比如调试相对困难、部分第三方库可能不兼容等,但随着.Net平台的发展(特别是.Net9之后的版本),AOT的支持会越来越完善。对于需要高性能、小体积、反编译保护的Avalonia应用来说,AOT发布是一个值得考虑的选择。

AOT编译Avalonia应用:StarBlog Publisher项目实践与挑战的更多相关文章

  1. 知乎问题之:.NET AOT编译后能替代C++吗?

    标题上的Native库是指:Native分为静态库( 作者:nscript链接:https://www.zhihu.com/question/536903224/answer/2522626086 ( ...

  2. MVC项目实践,在三层架构下实现SportsStore-01,EF Code First建模、DAL层等

    SportsStore是<精通ASP.NET MVC3框架(第三版)>中演示的MVC项目,在该项目中涵盖了MVC的众多方面,包括:使用DI容器.URL优化.导航.分页.购物车.订单.产品管 ...

  3. Spring cloud项目实践(一)

    链接地址:http://sail-y.github.io/2016/03/21/Spring-cloud%E9%A1%B9%E7%9B%AE%E5%AE%9E%E8%B7%B5/ 什么是Spring ...

  4. Asp.net MVC + EF + Spring.Net 项目实践3

    Asp.net MVC + EF + Spring.Net 项目实践 这一篇要整合Model层和Repository层,提供一个统一的操作entity的接口层,代码下载地址(博客园上传不了10M以上的 ...

  5. Django项目实践4 - Django网站管理(后台管理员)

    http://blog.csdn.net/pipisorry/article/details/45079751 上篇:Django项目实践3 - Django模型 Introduction 对于某一类 ...

  6. angular aot编译报错 ERROR in ./src/main.ts 解决方法

    昨天打包项目时遇到下图这样的错误: 开始以为了某些模块存在但未使用,折腾一番无果,后来升级angular-cli就搞定了,方法很简单: 1.删掉node_modules 2.更改package.jso ...

  7. Django项目实践4 - Django站点管理(后台管理员)

    http://blog.csdn.net/pipisorry/article/details/45079751 上篇:Django项目实践3 - Django模型 Introduction 对于某一类 ...

  8. Vue + webpack 项目实践

    Vue.js 是一款极简的 mvvm 框架,如果让我用一个词来形容它,就是 “轻·巧” .如果用一句话来描述它,它能够集众多优秀逐流的前端框架之大成,但同时保持简单易用.废话不多说,来看几个例子: & ...

  9. 【微信小程序项目实践总结】30分钟从陌生到熟悉 web app 、native app、hybrid app比较 30分钟ES6从陌生到熟悉 【原创】浅谈内存泄露 HTML5 五子棋 - JS/Canvas 游戏 meta 详解,html5 meta 标签日常设置 C#中回滚TransactionScope的使用方法和原理

    [微信小程序项目实践总结]30分钟从陌生到熟悉 前言 我们之前对小程序做了基本学习: 1. 微信小程序开发07-列表页面怎么做 2. 微信小程序开发06-一个业务页面的完成 3. 微信小程序开发05- ...

  10. nodejs, vue, webpack 项目实践

    vue 及 webpack,均不需要与nodejs一期使用,他们都可以单独使用到任何语言的框架中. http://jiongks.name/blog/just-vue/ https://cn.vuej ...

随机推荐

  1. .NET Core + Kafka 开发指南

    什么是Kafka Apache Kafka是一个分布式流处理平台,由LinkedIn开发并开源,后来成为Apache软件基金会的顶级项目.Kafka主要用于构建实时数据管道和流式应用程序. Kafka ...

  2. JVM-总结列表

    第一章 JVM内存结构 1.为什么要了解JVM内存管理机制 JVM自动的管理内存的分配与回收,这会在不知不觉中浪费很多内存,导致JVM花费很多时间去进行垃圾回收(GC) 内存泄露,导致JVM内存最终不 ...

  3. w3cschool-Lua编程入门

    https://www.w3cschool.cn/nhycto/ https://www.w3cschool.cn/cf_web/cf_web-dvxc32qu.html 1. Lua 基础知识 (1 ...

  4. Mybatis框架详解

    Mybatis框架(1)---Mybatis入门 mybatis入门   MyBatis是什么? MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache sof ...

  5. String类的使用1

    /*String:字符串,使用一对""引起来表示.1.String声明为final的,不可被继承2.String实现了Serializable接口:表示字符串是支持序列化的. 实现 ...

  6. 第一届启航杯网络安全大赛部分wp

    第一届启航杯 WEB Easy include <?php error_reporting(0); //flag in flag.php $file=$_GET['fil e']; if(iss ...

  7. vue平铺日历组件

    vue日历自定义平铺组件 <sy-icon @click="preMon" class="copy-icon" iconClass="iconj ...

  8. 微信小程序slot(一)

    在我们封装组件的时候,有些时候,我们需要使用类似于vue中的slot插槽: 小程序借鉴了这个优秀的想法: 在小程序中,组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子 ...

  9. linux下创建idea的桌面快捷方式

    !!!使用linux系统安装idea才会用到: 在桌面上,新建文件,命名为:idea.desktop , (或者在别的地方创建后再放到桌面) 使用 vim 编辑该文件(或者不新建,直接vi idea. ...

  10. FLink18--全窗口聚合方式2 ProcessWindowApp

    一.依赖 二.代码 package net.xdclass.class11; import java.util.List; import java.util.stream.Collectors; im ...