AOT使用經驗總結m8

ocenwimtaegrad發表於2024-11-06

一、引言

站長接觸 AOT 已有 3 個月之久,此前在《好訊息:NET 9 X86 AOT的突破 - 支援老舊Win7與XP環境》一文中就有所提及。在這段時間裡,站長使用 Avalonia 開發的專案也成功完成了 AOT 釋出測試。然而,這一過程並非一帆風順。站長在專案功能完成大半部分才開始進行 AOT 測試,期間遭遇了不少問題,可謂是 “踩坑無數”。為了方便日後回顧,也為了給廣大讀者提供參考,在此將這段經歷進行總結。

.NET AOT是將.NET程式碼提前編譯為本機程式碼的技術。其優勢眾多,啟動速度快,減少執行時資源佔用,還提高安全性。AOT釋出後無需再安裝.NET執行時等依賴。.NET 8、9 AOT釋出後,可在XP、Win7非SP1作業系統下執行。這使得應用部署更便捷,能適應更多老舊系統環境,為開發者擴充了應用場景,在效能提升的同時,也增加了系統相容性,讓.NET應用的開發和部署更具靈活性和廣泛性,給使用者帶來更好的體驗。

二、經驗之談

(一)測試策略的重要性

從專案建立伊始,就應養成良好的習慣,即只要新增了新功能或使用了較新的語法,就及時進行 AOT 釋出測試。否則,問題積累到後期,解決起來會異常艱難,站長就因前期忽視了這一點,付出了慘痛的代價。無奈的解決方法是重新建立專案,然後逐個還原功能並進行 AOT 測試。經過了一週的加班AOT測試,每個 AOT 釋出過程大致如下:

  1. 內網 AOT 釋出一次需 2、3 分鐘,這段時間只能看看需求文件、技術文章、需求文件、技術文章。。。
  2. 釋出完成,執行無效果,體現在雙擊未出現介面,程序列表沒有它,說明程式崩潰了,檢視系統應用事件日誌,日誌中通常會包含異常警告資訊。
  3. 依據日誌資訊檢查程式碼,修改相關 API。
  4. 再次進行 AOT 釋出,重複上述 1 - 3 步驟。

經過一週的努力,專案 AOT 後功能測試終於正常,至此收工。

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

1. 新增rd.xml

在主工程建立一個XML檔案,例如Roots.xml,內容大致如下:



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


需要支援AOT的工程,在該XML中新增一個assembly節點,fullname是程式集名稱,CodeWF.Toolbox.Desktop是站長小工具的主工程名,點選檢視原始碼。

在主工程新增ItemGroup節點關聯該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需新增如下內容:



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


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

4. HttpClient使用

rd.xml新增如下內容:



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


5. Dapper支援

Dapper的AOT支援需要安裝Dapper.AOT包,rd.xml新增如下內容:



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


資料庫操作的方法需要新增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(json!, options); |
|  | errorMsg = default; |
|  | return true; |
|  | } |
|  | catch (Exception ex) |
|  | { |
|  | obj = default; |
|  | errorMsg = ex.Message; |
|  | return false; |
|  | } |
|  | } |


7. 反射問題

參考專案CodeWF.NetWeaver

  1. 建立指定型別的ListDictionary例項:


|  | 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. 反射呼叫ListDictionaryAdd方法新增元素失敗,下面是虛擬碼:


|  | // List |
| --- | --- |
|  | var addMethod = type.GetMethod("Add"); |
|  | addMethod.Invoke(obj, new[]{ child }) |
|  |  |
|  | // Dictionary |
|  | var addMethod = type.GetMethod("Add"); |
|  | addMethod.Invoke(obj, new[]{ key, value }) |


解決辦法,轉換為實現的介面呼叫:



|  | // List |
| --- | --- |
|  | (obj as IList).Add(child); |
|  |  |
|  | // Dictionary |
|  | (obj as IDictionary)[key] = value; |


  1. 獲取陣列、ListDictionary的元素個數

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



|  | 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成功:直接透過轉換為基型別或實現的介面呼叫屬性即可:



|  | // 陣列 |
| --- | --- |
|  | var length = ((Array)value).Length; |
|  |  |
|  | // List |
|  | if (value is IList list) |
|  | { |
|  | var count = list.Count; |
|  | } |
|  |  |
|  | // Dictionary |
|  | if (value is IDictionary dictionary) |
|  | { |
|  | var count = dictionary.Count; |
|  | } |


8. Windows 7支援

如遇AOT後無法在Windows 7執行,請新增YY-Thunks包:



|  | "YY-Thunks" Version="1.1.4-Beta3" /> |
| --- | --- |


並指定目標框架為net9.0-windows

9. Winform\相容XP

如果第8條後還執行不了,請參考上一篇文章《.NET 9 AOT的突破 - 支援老舊Win7與XP環境 - 碼界工坊 (dotnet9.com):FlowerCloud機場》新增VC-LTL包,這裡不贅述。

10. 其他

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

三、總結

AOT 釋出測試雖然過程中可能會遇到諸多問題,但透過及時的測試和正確的配置調整,最終能夠實現專案的順利釋出。希望以上總結的經驗能對大家在 AOT 使用過程中有所幫助,讓大家在開發過程中少走彎路,提高專案的開發效率和質量。同時,也期待大家在實踐中不斷探索和總結,共同推動技術的進步和發展。

AOT可參考專案:

  • CodeWF.NetWeaver: https://github.com/dotnet9/CodeWF.NetWeaver
  • CodeWF.Tools:https://github.com/dotnet9/CodeWF.Tools
  • CodeWF.Toolbox:https://github.com/dotnet9/CodeWF.Toolbox

相關文章