AOT编译Avalonia应用:StarBlog Publisher项目实践与挑战

前言

最近我使用 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相关的配置:

xml 复制代码
<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 的话直接跳过。

c# 复制代码
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源生成器:

xml 复制代码
<PropertyGroup>
    <!-- 其他属性 -->
    <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>

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

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

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

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

添加类型预注册

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

c# 复制代码
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类,确保在初始化时注册类型:

c# 复制代码
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中初始化类型注册

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

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

添加AOT兼容性配置

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

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文件:

xml 复制代码
<ItemGroup>
    <RdXmlFile Include="rd.xml" />
</ItemGroup>

发布

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

bash 复制代码
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发布是一个值得考虑的选择。

相关推荐
gc_22995 小时前
C#测试Excel开源组件ExcelDataReader
c#·excel·exceldatareader
勘察加熊人7 小时前
c#使用forms实现helloworld和login登录
开发语言·c#
我不是程序猿儿8 小时前
【C#】设备回帧太快、数据没收完整就被读取,导致帧被拆、混、丢,很常见,尤其在高频通信设备,解决方案
开发语言·c#
闪电麦坤959 小时前
C#:尝试解析方法TryParse
开发语言·c#
我不是程序猿儿9 小时前
【C#】构造协议帧通过串口下发
开发语言·c#
白烟染黑墨11 小时前
抽离BlazorWebview中的.Net与Javascript的互操作库
c#·客户端开发
小样vvv13 小时前
【分布式】深入剖析 Sentinel 限流:原理、实现
分布式·c#·sentinel
闪电麦坤9513 小时前
C#:常见 Console 类输入输出方法
开发语言·c#
勘察加熊人15 小时前
c#使用wpf实现helloworld和login登录
开发语言·c#·wpf
鲤籽鲲17 小时前
C# System.Net.Dns 使用详解
网络·c#·.net