AOT使用經(jīng)驗(yàn)總結(jié)

一、引言

站長接觸 AOT 已有 3 個(gè)月之久,此前在《好消息:NET 9 X86 AOT的突破 - 支持老舊Win7與XP環(huán)境》一文中就有所提及。在這段時(shí)間里,站長使用 Avalonia 開發(fā)的項(xiàng)目也成功完成了 AOT 發(fā)布測(cè)試。然而,這一過程并非一帆風(fēng)順。站長在項(xiàng)目功能完成大半部分才開始進(jìn)行 AOT 測(cè)試,期間遭遇了不少問題,可謂是 “踩坑無數(shù)”。為了方便日后回顧,也為了給廣大讀者提供參考,在此將這段經(jīng)歷進(jìn)行總結(jié)。

.NET AOT是將.NET代碼提前編譯為本機(jī)代碼的技術(shù)。其優(yōu)勢(shì)眾多,啟動(dòng)速度快,減少運(yùn)行時(shí)資源占用,還提高安全性。AOT發(fā)布后無需再安裝.NET運(yùn)行時(shí)等依賴。.NET 8、9 AOT發(fā)布后,可在XP、Win7非SP1操作系統(tǒng)下運(yùn)行。這使得應(yīng)用部署更便捷,能適應(yīng)更多老舊系統(tǒng)環(huán)境,為開發(fā)者拓展了應(yīng)用場(chǎng)景,在性能提升的同時(shí),也增加了系統(tǒng)兼容性,讓.NET應(yīng)用的開發(fā)和部署更具靈活性和廣泛性,給用戶帶來更好的體驗(yàn)。

二、經(jīng)驗(yàn)之談

(一)測(cè)試策略的重要性

從項(xiàng)目創(chuàng)建伊始,就應(yīng)養(yǎng)成良好的習(xí)慣,即只要添加了新功能或使用了較新的語法,就及時(shí)進(jìn)行 AOT 發(fā)布測(cè)試。否則,問題積累到后期,解決起來會(huì)異常艱難,站長就因前期忽視了這一點(diǎn),付出了慘痛的代價(jià)。無奈的解決方法是重新創(chuàng)建項(xiàng)目,然后逐個(gè)還原功能并進(jìn)行 AOT 測(cè)試。經(jīng)過了一周的加班AOT測(cè)試,每個(gè) AOT 發(fā)布過程大致如下:

  1. 內(nèi)網(wǎng) AOT 發(fā)布一次需 2、3 分鐘,這段時(shí)間只能看看需求文檔、技術(shù)文章、需求文檔、技術(shù)文章。。。
  2. 發(fā)布完成,運(yùn)行無效果,體現(xiàn)在雙擊未出現(xiàn)界面,進(jìn)程列表沒有它,說明程序崩潰了,查看系統(tǒng)應(yīng)用事件日志,日志中通常會(huì)包含異常警告信息。
  3. 依據(jù)日志信息檢查代碼,修改相關(guān) API。
  4. 再次進(jìn)行 AOT 發(fā)布,重復(fù)上述 1 - 3 步驟。

經(jīng)過一周的努力,項(xiàng)目 AOT 后功能測(cè)試終于正常,至此收工。

(二)AOT 需要注意的點(diǎn)及解決方法

1. 添加rd.xml

在主工程創(chuàng)建一個(gè)XML文件,例如Roots.xml,內(nèi)容大致如下:

<linker>
    <assembly fullname="CodeWF.Toolbox.Desktop" preserve="All" />
</linker>

需要支持AOT的工程,在該XML中添加一個(gè)assembly節(jié)點(diǎn),fullname是程序集名稱,CodeWF.Toolbox.Desktop是站長小工具的主工程名,點(diǎn)擊查看源碼。

在主工程添加ItemGroup節(jié)點(diǎn)關(guān)聯(lián)該XML文件:

<ItemGroup>
    <TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>

2. Prism支持

站長使用了Prism框架及DryIOC容器,若要支持 AOT,需要添加以下 NuGet 包:

<PackageReference Include="Prism.Avalonia" Version="8.1.97.11073" />
<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11073" />

rd.xml需要添加

<assembly fullname="Prism" preserve="All" />
<assembly fullname="DryIoc" preserve="All" />
<assembly fullname="Prism.Avalonia" preserve="All" />
<assembly fullname="Prism.DryIoc.Avalonia" preserve="All" />

3. App.config讀寫

在.NET Core中使用System.Configuration.ConfigurationManager包操作App.config文件,rd.xml需添加如下內(nèi)容:

<assembly fullname="System.Configuration.ConfigurationManager" preserve="All" />

使用Assembly.GetEntryAssembly().location失敗,目前使用ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None)獲取的應(yīng)用程序程序配置,指定路徑的方式后續(xù)再研究。

4. HttpClient使用

rd.xml添加如下內(nèi)容:

<assembly fullname="System.Net.Http" preserve="All" />

5. Dapper支持

Dapper的AOT支持需要安裝Dapper.AOT包,rd.xml添加如下內(nèi)容:

<assembly fullname="Dapper" preserve="All" />
<assembly fullname="Dapper.AOT" preserve="All" />

數(shù)據(jù)庫操作的方法需要添加DapperAOT特性,舉例如下:

[DapperAot]
public static bool EnsureTableIsCreated()
{
    try
    {
        using var connection = new SqliteConnection(DBConst.DBConnectionString);
        connection.Open();

        const string sql = $@"
            CREATE TABLE IF NOT EXISTS {nameof(JsonPrettifyEntity)}(
                {nameof(JsonPrettifyEntity.IsSortKey)} Bool,
                {nameof(JsonPrettifyEntity.IndentSize)} INTEGER
        )";

        using var command = new SqliteCommand(sql, connection);
        return command.ExecuteNonQuery() > 0;
    }
    catch (Exception ex)
    {
        return false;
    }
}

6. System.Text.Json

參考JsonExtensions.cs

序列化

public static bool ToJson<T>(this T obj, out string? json, out string? errorMsg)
{
    if (obj == null)
    {
        json = default;
        errorMsg = "Please provide object";
        return false;
    }

    var options = new JsonSerializerOptions()
    {
        WriteIndented = true,
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    };
    try
    {
        json = JsonSerializer.Serialize(obj, options);
        errorMsg = default;
        return true;
    }
    catch (Exception ex)
    {
        json = default;
        errorMsg = ex.Message;
        return false;
    }
}

反序列化

public static bool FromJson<T>(this string? json, out T? obj, out string? errorMsg)
{
    if (string.IsNullOrWhiteSpace(json))
    {
        obj = default;
        errorMsg = "Please provide json string";
        return false;
    }

    try
    {
        var options = new JsonSerializerOptions()
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        };
        obj = JsonSerializer.Deserialize<T>(json!, options);
        errorMsg = default;
        return true;
    }
    catch (Exception ex)
    {
        obj = default;
        errorMsg = ex.Message;
        return false;
    }
}

7. 反射問題

參考項(xiàng)目CodeWF.NetWeaver

  1. 創(chuàng)建指定類型的List<T>Dictionary<T>實(shí)例:
public static object CreateInstance(Type type)
{
    var itemTypes = type.GetGenericArguments();
    if (typeof(IList).IsAssignableFrom(type))
    {
        var lstType = typeof(List<>);
        var genericType = lstType.MakeGenericType(itemTypes.First());
        return Activator.CreateInstance(genericType)!;
    }
    else
    {
        var dictType = typeof(Dictionary<,>);
        var genericType = dictType.MakeGenericType(itemTypes.First(), itemTypes[1]);
        return Activator.CreateInstance(genericType)!;
    }
}
  1. 反射調(diào)用List<T>Dictionary<T>Add方法添加元素失敗,下面是偽代碼:
// List<T>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ child })
    
// Dictionary<Key, Value>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ key, value })

解決辦法,轉(zhuǎn)換為實(shí)現(xiàn)的接口調(diào)用:

// List<T>
(obj as IList).Add(child);

// Dictionary<Key, Value>
(obj as IDictionary)[key] = value;
  1. 獲取數(shù)組、List<T>Dictionary<key, value>的元素個(gè)數(shù)

同上面Add方法反射獲取Length或Count屬性皆返回0,value.Property("Length", 0),封裝的Property非AOT運(yùn)行正確:

public static T Property<T>(this object obj, string propertyName, T defaultValue = default)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(nameof(propertyName));

    var propertyInfo = obj.GetType().GetProperty(propertyName);
    if (propertyInfo == null)
    {
        return defaultValue;
    }

    var value = propertyInfo.GetValue(obj);

    try
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }
    catch (InvalidCastException)
    {
        return defaultValue;
    }
}

AOT成功:直接通過轉(zhuǎn)換為基類型或?qū)崿F(xiàn)的接口調(diào)用屬性即可:

// 數(shù)組
var length = ((Array)value).Length;

// List<T>
 if (value is IList list)
{
    var count = list.Count;
}

// Dictionary<key, value>
if (value is IDictionary dictionary)
{
    var count = dictionary.Count;
}

8. Windows 7支持

如遇AOT后無法在Windows 7運(yùn)行,請(qǐng)?zhí)砑?code>YY-Thunks包:

<PackageReference Include="YY-Thunks" Version="1.1.4-Beta3" />

并指定目標(biāo)框架為net9.0-windows

9. Winform\兼容XP

如果第8條后還運(yùn)行不了,請(qǐng)參考上一篇文章《.NET 9 AOT的突破 - 支持老舊Win7與XP環(huán)境 - 碼界工坊 (dotnet9.com)》添加VC-LTL包,這里不贅述。

10. 其他

還有許多其他需要注意的地方,后續(xù)想起來逐漸完善本文。

三、總結(jié)

AOT 發(fā)布測(cè)試雖然過程中可能會(huì)遇到諸多問題,但通過及時(shí)的測(cè)試和正確的配置調(diào)整,最終能夠?qū)崿F(xiàn)項(xiàng)目的順利發(fā)布。希望以上總結(jié)的經(jīng)驗(yàn)?zāi)軐?duì)大家在 AOT 使用過程中有所幫助,讓大家在開發(fā)過程中少走彎路,提高項(xiàng)目的開發(fā)效率和質(zhì)量。同時(shí),也期待大家在實(shí)踐中不斷探索和總結(jié),共同推動(dòng)技術(shù)的進(jìn)步和發(fā)展。

AOT可參考項(xiàng)目:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容