更新紀錄(1 筆,最後更新 2026年09月04日)
本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。
- 已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.21616272)
- 初次發布
引用本文(DOI: 10.5281/zenodo.21616271)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
Go Komura(2026)。〈.NET Native AOT 是什麼 - 與 JIT、trimming 的差異〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616271 https://comcomponent.com/zh-TW/blog/2026/03/13/001-dotnet-native-aot-what-is/
- DOI(最新版本)
- 10.5281/zenodo.21616271
- DOI(此版本)
- 10.5281/zenodo.22297109
之前在 從 C/C++ 呼叫 C# Native AOT DLL 的方法 裡,已經寫過用 Native AOT 從 C/C++ 呼叫 C# 的做法。 不過說實在的,先擺一篇 Native AOT 到底是什麼 會比較貼心。順序稍微顛倒了一點。
Native AOT 這個話題,一開始最容易把用語攪在一起。
- 這是在講把 JIT 拿掉嗎
- 和 self-contained 或 single-file 差在哪裡
- 和 ReadyToRun 是同一個系統嗎
- trimming warning 大量冒出來時到底發生了什麼事
- WPF / WinForms / ASP.NET Core 是不是都能用同樣的認真程度去套
這些一旦攪在一起,Native AOT 就會看起來像「只是變快的魔法」,或者反過來像「滿是限制又很可怕的東西」。兩種看法都有點草率。
flowchart TB
accTitle: 用語攪在一起時產生的誤解
accDescr: JIT、self-contained、ReadyToRun、trimming 這些用語一旦攪在一起,Native AOT 就會看起來像只是變快的魔法或滿是限制的可怕東西,但兩種看法都很草率的圖。
mix["用語攪在一起"] --> m1["看起來像變快的魔法"]
mix --> m2["看起來滿是限制很可怕"]
sort["把用語分開梳理"] --> fair["變成看得見使用時機的選項"]
圖 1: 誤解的來源是用語混線。先從把用語分開開始。
這篇文章以 .NET 8 之後的現況實務感受為前提,先把下面四點梳理清楚。
- Native AOT 的真面目
- 好處在哪裡,哪裡會變吃力
- 和 ReadyToRun、trimming 差在哪裡
- 從哪一種應用程式開始試比較平穩
目錄
- 先講結論(一句話)
- 先看的整理表
- 2.1. Native AOT 周邊的用語
- 2.2. JIT / ReadyToRun / Native AOT 的差異
- Native AOT 的全貌(圖)
- Native AOT 的好處
- 4.1. 啟動容易變輕
- 4.2. 不必以事先安裝執行階段為前提
- 4.3. 適合受限的執行環境
- Native AOT 會變吃力的地方
- 5.1. 反射與動態產生程式碼
- 5.2. 必須以 trimming 為前提思考
- 5.3. 依平台分別發行
- 5.4. Windows 桌面 / COM 情境要相當謹慎
- 最小步驟
- 6.1.
csproj - 6.2. publish
- 6.3. JSON 的寫法
- 6.1.
- 適合的情境
- 不適合的情境
- 常見的卡關處
- 總結
- 參考資料
圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 16 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle
1. 先講結論(一句話)
- Native AOT 是在 publish 時把 .NET 應用程式事前編譯成原生程式碼再散發的方式。
- 因為不使用執行階段 JIT,啟動時間與記憶體占用容易變好,也比較容易散發到沒有安裝 .NET 執行階段的環境。
- 不過,它和自由的反射、動態產生程式碼、built-in COM,以及不支援 trimming 的函式庫都合不來。
- 也就是說,與其說是變快的魔法,不如說是為了啟動、散發與執行環境的需求,捨掉一部分動態世界而往靜態世界靠攏的發行模型。
Native AOT 是「把 .NET 用接近原生的方式散發出去的機制」,並不是一個打勾就讓編譯變快的核取方塊。
flowchart TB
accTitle: 得到什麼與捨棄什麼
accDescr: Native AOT 透過 publish 時的事前編譯得到啟動時間、記憶體占用與不需執行階段的散發,代價是捨棄自由的反射、動態產生程式碼與 built-in COM 的相容性的圖。
aot["Native AOT"] --> gain["得到的東西"]
aot --> lose["捨棄的東西"]
gain --> g1["啟動、記憶體與散發都變輕"]
lose --> l1["自由的反射"]
lose --> l2["動態產生程式碼與 built-in COM"]
圖 2: 不是變快的魔法,而是捨掉一部分動態世界而往靜態世界靠攏的交換。
2. 先看的整理表
2.1. Native AOT 周邊的用語
一開始先把這幾個分開,後面會輕鬆很多。
| 用語 | 做什麼用的 | 與 Native AOT 的關係 |
|---|---|---|
| JIT | 在執行階段從 IL 產生原生程式碼 | Native AOT 把這一段事先做完 |
| self-contained | 把執行所需的 .NET 一整套一起散發 | Native AOT 屬於這個系統 |
| single-file | 把散發產物合併成 1 個檔案 | 和 Native AOT 的本質是兩回事,但結果的外觀容易變得接近 |
| trimming | 削掉沒有用到的程式碼 | 在 Native AOT 幾乎是前提 |
| ReadyToRun | 保留 IL,只把 JIT 的工作稍微提前 | 和 Native AOT 看似相近,其實是另一回事 |
| source generator | 把執行階段的動態處理移到建置期產生程式碼 | 和 Native AOT 很搭 |
容易混淆的地方在於,Native AOT 與其說是一個功能,不如說是一種發行模型:它會和 self-contained、trimming、source generation、固定 RID 的 publish 搭在一起運作。
flowchart TB
accTitle: 和 Native AOT 一起運作的機制
accDescr: Native AOT 不是單一功能,而是和 self-contained、trimming、source generation、固定 RID 的 publish 組合起來運作的發行模型的圖。
aot["Native AOT〔發行模型〕"] --> s1["self-contained 的系統"]
aot --> s2["trimming 幾乎是前提"]
aot --> s3["和 source generator 很搭"]
aot --> s4["以固定 RID 進行 publish"]
圖 3: Native AOT 不是單獨的功能,而是和周邊機制成套運作的發行模型。
2.2. JIT / ReadyToRun / Native AOT 的差異
這裡也是一開始用一張表看比較快。
| 觀點 | 一般的 JIT 執行 | ReadyToRun | Native AOT |
|---|---|---|---|
| 執行階段 JIT | 使用 | 仍有會用到的場合 | 不使用 |
| 散發產物的內容 | 以 IL 為主 | IL 加上事先產生的程式碼 | 以原生執行檔為主 |
| 啟動 | 基準 | 容易改善 | 相當容易改善 |
| 相容性 | 最廣 | 廣 | 限制較強 |
| 動態功能 | 好用 | 大致上好用 | 限制很多 |
| 適合的目的 | 一般的 .NET 開發 | 想先改善啟動 | 想強力拿下啟動、散發與受限環境 |
如果說 ReadyToRun 走的是「讓 JIT 輕鬆一點」的方向,那麼 Native AOT 走的就是「不以執行階段 JIT 本身為前提」的方向。 即使名稱裡都帶著 AOT,兩者的認真程度差很多。
flowchart TB
accTitle: ReadyToRun 和 Native AOT 的方向差異
accDescr: ReadyToRun 保留 IL 只把 JIT 的工作稍微提前讓它輕鬆一點,Native AOT 則不以執行階段 JIT 本身為前提,即使都叫 AOT 認真程度也差很多的圖。
q{"想怎麼處理 JIT"}
q -->|"讓它輕鬆一點"| r2r["ReadyToRun〔IL 保留下來〕"]
q -->|"不以它為前提"| aot["Native AOT〔以原生為主〕"]
r2r --> soft["相容性維持得很廣"]
aot --> hard["啟動快但限制強"]
圖 4: 同樣叫 AOT,「讓 JIT 輕鬆」和「不以 JIT 為前提」是兩回事。
在這個基礎上,把「那到底要選哪一種散發形態」畫成一張圖,會是這樣。
flowchart TD
S["想決定散發形態"] --> Q1{"能不能在散發目的地裝 .NET 執行階段?"}
Q1 -- "能裝" --> Q2{"想壓低啟動時間嗎?"}
Q2 -- "沒有到那個程度" --> P1["framework-dependent<br/>一般的發行"]
Q2 -- "想壓低" --> P2["ReadyToRun<br/>維持相容性同時改善啟動"]
Q1 -- "不想裝 / 裝不了" --> Q3{"是否依賴 reflection、動態產生程式碼、built-in COM?"}
Q3 -- "依賴" --> P3["self-contained<br/>必要時用 single-file 合併"]
Q3 -- "不依賴" --> Q4{"對象是 console / worker / 小型 API?"}
Q4 -- "是" --> P4["Native AOT"]
Q4 -- "否" --> P5["先 self-contained<br/>減少動態相依之後再重新評估"]
圖 5: 散發形態的選法。依序判斷能不能放執行階段,再看能不能減少動態機制。
第一個分歧是 能不能把執行階段放到散發目的地,第二個分歧是 動態機制能減少到什麼程度。 self-contained 和 single-file 講的是「怎麼散發」,ReadyToRun 和 Native AOT 講的是「什麼時候產生原生程式碼」,實務上會把兩邊組合起來考慮。
3. Native AOT 的全貌(圖)
把 Native AOT 粗略地畫成圖,會是這樣。
flowchart LR
Src["C# / .NET 原始碼"] --> IL["IL 組件"]
IL -->|一般執行| JIT["執行階段 JIT"]
JIT --> Run1["應用程式執行"]
IL -->|dotnet publish + PublishAot| Analyze["AOT / trim 分析"]
Analyze --> Trim["削減不需要的程式碼"]
Trim --> AOT["產生原生程式碼"]
AOT --> Run2["RID 專屬的執行檔"]
圖 6: 一般執行是在執行階段做 JIT,Native AOT 則把分析、削減與原生產生提前到 publish 時。
平常的 .NET 會先產生 IL,再在執行階段只對需要的部分做 JIT。 Native AOT 則把後段很大一部分提前到 publish 時完成。
這時重要的是,在 publish 的時間點必須「幾乎完全知道執行階段會用到哪些程式碼」。 到這裡,允許的寫法前提就變了。
- 在執行階段尋找型別
- 在執行階段長出程式碼
- 在執行階段載入 Assembly
- 抱著「總會有辦法」的心態延後解析
這類寫法會突然變得和 Native AOT 很不合。
flowchart TB
accTitle: publish 時必須全部知道
accDescr: Native AOT 在 publish 的時間點必須幾乎完全知道執行階段會用到哪些程式碼,因此在執行階段尋找型別、長出程式碼、載入 Assembly 或延後解析的寫法都會變得很不合的圖。
need["publish 時確定所需程式碼"] --> ng1["在執行階段尋找型別"]
need --> ng2["在執行階段長出程式碼"]
need --> ng3["在執行階段讀取 Assembly"]
need --> ng4["靠延後解析撐過去"]
ng1 -.-> bad["每一種都和 AOT 不合"]
圖 7: 前提改變的核心。越是「在執行階段才決定」的寫法,越會和 Native AOT 衝突。
4. Native AOT 的好處
4.1. 啟動容易變輕
Native AOT 最容易看出來的效果,還是啟動。
- CLI 工具
- 短命處理程序
- 類似無伺服器的啟動
- 容器的啟動與替換
- 監控工具或小型常駐處理程序
在這些場合 JIT 的成本很明顯,Native AOT 把那一段提前做完,起步就會比較輕。
記憶體占用也容易改善,所以在想把同樣台數塞得更滿的場合也有利。 特別是在同一個處理程序會大量啟動的雲端側,這個差距會逐漸拉開。
4.2. 不必以事先安裝執行階段為前提
用 Native AOT publish 的應用程式,在沒有安裝 .NET 執行階段的環境也比較容易跑起來。
這一點不起眼卻很有用。
- 不想跟散發目的地說「請先安裝 .NET 9 Runtime」
- 想讓容器映像檔更精簡
- 想只放一支小工具就讓它動起來
- 不想/不能在執行環境上允許 JIT
在這些場合,光是少了「要另外備妥執行階段」這個前提,事情就安靜很多。
這裡說的「不需要執行階段」,意思是不必在散發目的地另外安裝 .NET。 並不是說應用程式裡面連相當於 runtime 的部分都完全消失。
flowchart TB
accTitle: 不需要執行階段的正確意思
accDescr: Native AOT 所說的不需要執行階段是指不必在散發目的地另外安裝 .NET,並不是指應用程式裡面相當於 runtime 的部分完全消失的圖。
word["不需要執行階段這句話"] --> ok["不必在散發目的地安裝 .NET"]
word -.-> ngx["並不是相當於 runtime 的部分消失"]
ok --> merit["散發與啟動的前提變少"]
圖 8: 「不需要執行階段」講的是散發目的地。相當於 runtime 的部分仍包含在應用程式裡。
4.3. 適合受限的執行環境
Native AOT 不使用執行階段 JIT,所以在不允許 JIT 的環境也比較容易跑起來。
這一點比起 desktop,在雲端、容器、行動裝置這一側更容易發揮。 不過在 Windows 開發的情境裡,就「減少散發目的地多餘的前提」這個意義來說,也是相當令人開心的優點。
5. Native AOT 會變吃力的地方
5.1. 反射與動態產生程式碼
Native AOT 最核心的限制就在這裡。
Assembly.LoadFile這類動態載入System.Reflection.Emit這類執行階段產生程式碼- 在執行階段無限制地追型別的反射
- 在執行階段隨意組出 generic 的寫法
這些在 publish 時不容易確定需要哪些程式碼,所以是 AOT warning 的溫床。
當然,也沒有單純到「只要有一行反射就立刻出局」。 不過,越是偏向「在執行階段看了再決定」的設計就越吃力,這個傾向不會落空。
warning 名稱裡常出現的是 RequiresDynamicCode 系列。
它的意思是「這個呼叫在 AOT 下可能會壞掉」,所以不要隨手 suppress 比較安全。
做 Native AOT 的時候,可以把它想成是減少「執行階段的聰明」、增加「建置期的明示」的方向,這樣比較好理解。
flowchart TB
accTitle: 從執行階段的聰明轉向建置期的明示
accDescr: 動態載入、執行階段產生程式碼與無限制的反射都是 AOT warning 的溫床,因此要減少執行階段的聰明、增加建置期的明示,把設計往那個方向靠攏的圖。
dynamicway["依賴執行階段聰明的設計"] --> warn["RequiresDynamicCode 系列的 warning"]
warn --> shift["增加建置期的明示"]
shift --> calm["publish 時確定所需程式碼"]
warn -.-> nosup["不要隨手 suppress"]
圖 9: 面對核心限制的方向只有一個。把「在執行階段看了再決定」換成「在建置期明示」。
5.2. 必須以 trimming 為前提思考
Native AOT 和 trimming 綁得很深。 這裡容易漏看的是,不只自己的程式碼,相依函式庫那一側的寫法也會產生影響。
要特別注意的是這幾類。
- 以反射為基礎的序列化器
- 以執行階段掃描收集型別的 DI/外掛結構
- 從字串名稱找型別再建立執行個體的機制
- 偏向動態 proxy 或 IL 產生的函式庫
明明出現了 warning 卻覺得「publish 過了就好」,之後會很難收拾。 在 Native AOT,warning 大致上都值得認真讀。
flowchart TB
accTitle: trimming 的成敗不是只看自己的程式碼
accDescr: trimming 不只自己的程式碼,相依函式庫那一側的寫法也會產生影響,以反射為基礎的序列化器、執行階段掃描的 DI、從字串名稱解析型別、偏向動態 proxy 的函式庫都要特別注意的圖。
trim["trimming 的成敗"] --> own["自己的程式碼寫法"]
trim --> dep["相依函式庫的寫法"]
dep -.-> ex["反射系或執行階段掃描系"]
dep --> care["認真讀 warning 並確認"]
圖 10: 容易漏看的是相依函式庫那一側。「publish 過了就好」之後會很難收拾。
話雖如此,只講「認真讀」也動不了,所以這裡寫下處理的優先順序。 官方文件也建議照這個順序去試。
| 順序 | 處理方式 | 使用場合 |
|---|---|---|
| 1 | 不要用 reflection | 可以把 Activator.CreateInstance(Type) 換成 generic 引數,或是改用 source generator |
| 2 | 加上 DynamicallyAccessedMembers |
需要 reflection,但對象型別在編譯時就已經知道 |
| 3 | 加上 RequiresUnreferencedCode |
型別名稱是用執行階段的字串決定,本質上無法做靜態分析 |
| 4 | 用 UnconditionalSuppressMessage 抑制 |
上面全都做不到,而且已確認安全時的最後手段 |
第一步最常見的是第 2 種「型別明明已經知道,卻只是冒出警告」的情況。
例如下面的程式碼會出現 IL2070。
// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
只要用屬性宣告「既然要呼叫 GetMethods(),就請保留 public 方法」,警告就會消失。
using System.Diagnostics.CodeAnalysis;
void PrintMethodNames(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
// 呼叫端如果是用 typeof 傳入,需求就會自動被滿足
PrintMethodNames(typeof(DateTime));
呼叫的 API 和需要的指定大致上是對應的。GetMethod / GetMethods 對應 PublicMethods,GetProperty / GetProperties 對應 PublicProperties,Activator.CreateInstance 對應 PublicParameterlessConstructor 或 PublicConstructors。
DynamicallyAccessedMemberTypes.All 雖然輕鬆,但會把對象型別的成員整批保留而讓體積膨脹,保留下來的成員也會帶出別的 warning,所以原則是 只指定必要的最小範圍。
另外,加了屬性 warning 還是不消失時,要 從使用 reflection 的地方往呼叫端回溯,確認整條路徑上都有加。中間只要漏掉一處,需求就會在那裡斷掉。
flowchart TB
accTitle: 加了屬性 warning 還是不消失時
accDescr: DynamicallyAccessedMembers 要指定必要的最小範圍,warning 不消失時要從使用 reflection 的地方往呼叫端回溯,確認整條路徑上都有加屬性,只要漏掉一處需求就會在那裡斷掉的圖。
warnleft["加了屬性 warning 仍留著"] --> back["往呼叫端回溯"]
back --> chain["確認整條路徑上都有加"]
chain -.-> cut["漏掉一處需求就會斷掉"]
warnleft -.-> minrule["指定必要的最小範圍是原則"]
圖 11: 屬性要加在整條路徑上而不是單一點。中間漏掉一處,需求就會在那裡斷掉。
5.3. 依平台分別發行
Native AOT 是以固定 RID(Runtime Identifier)的方式 publish。
也就是說,這不是把為 win-x64 做出來的東西直接拿到 linux-x64 上跑的世界。
- Windows x64
- Windows Arm64
- Linux x64
- Linux Arm64
- macOS Arm64
要以像這樣為每個目標分別產生發行物為前提。
這裡的感覺會比一般 framework-dependent 的 .NET,更接近「原生應用程式」。
5.4. Windows 桌面 / COM 情境要相當謹慎
在 KomuraSoft 的情境裡,這一節特別重要。
在 Windows 上,Native AOT 沒有 built-in COM。 再加上 WPF 和 trimming 合不來,WinForms 又重度依賴 built-in COM marshalling,所以至少在現階段,兩者作為「第一個 Native AOT 候選」都應該相當謹慎看待。
這裡先寫下所謂「現階段」的基準。 本文的敘述是以 .NET 8 / 9 / 10 世代的官方文件(截至 2026 年 7 月)為依據。 關於 WPF 和 WinForms,Microsoft 的「Known trimming incompatibilities」明確寫著:WPF 對反射與執行階段程式碼檢查的依賴很強,trimming 之後幾乎跑不動;WinForms 則對 built-in COM marshalling 的依賴很重,因此 兩者都在 .NET SDK 這一側停用了 trimming 支援。 也就是說,現況不是「最好謹慎看待」,而是 SDK 已經把它擋下來了。將來這段敘述有可能改變,所以請在閱讀的當下確認同一個頁面。
flowchart TB
accTitle: WPF 和 WinForms 被擋下來的理由
accDescr: Windows 的 Native AOT 沒有 built-in COM,WPF 因為依賴反射與執行階段程式碼檢查在 trimming 之後幾乎跑不動,WinForms 則對 built-in COM marshalling 的依賴很重,因此兩者都在 .NET SDK 這一側停用了 trimming 支援的圖。
win["Windows 的 Native AOT"] --> nocom["沒有 built-in COM"]
wpf["WPF"] --> refl["對反射的依賴很強"]
wf["WinForms"] --> commar["對 COM marshalling 的依賴很重"]
refl --> off["SDK 這一側停用 trimming"]
commar --> off
圖 12: 不是「謹慎看待」,現況是 SDK 已經擋下來。不要把桌面本體當第一個候選。
總之,
- 直接把 WPF / WinForms 本體 Native AOT 化
- 用平常的感覺把 COM interop 原封不動帶進來
這些做法會讓 publish 時的 warning 與執行階段的限制一口氣增加,處理成本容易暴增。
反過來,
- 主控台
- worker
- 小型 Web API
- 原生介接之中比較容易收攏到 C 函式邊界的元件
這些當作入口比較直接了當。
如果需要 COM,維持 JIT,或是以 ComWrappers / source-generated COM 為前提重新設計,有些情況下方向會比較對。
6. 最小步驟
在那之前,Native AOT 的 publish 需要原生的工具鏈。
只加上 PublishAot 就打 dotnet publish,會失敗在最後的原生連結,而不是編譯。這是第一道關卡,所以請先裝好。
| 環境 | 需要的東西 |
|---|---|
| Windows | Visual Studio 2022 以後版本。安裝「使用 C++ 的桌面開發」工作負載,並把預設的元件全部裝上 |
| Ubuntu 18.04 以後 | sudo apt-get install clang zlib1g-dev |
| Alpine 3.15 以後 | sudo apk add clang build-base zlib-dev |
| Fedora 39 以後 / RHEL 8 以後 | sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel |
| macOS | Xcode 的 Command Line Tools(.NET 8 以後支援) |
總之就是編譯器工具鏈,以及 .NET 執行階段所依賴的函式庫的開發用套件。 如果要在 CI 上跑,dotnet/samples 的 Native AOT 範例裡有 Linux 和 Windows 兩邊的 Dockerfile,從那裡把前提的安裝步驟搬過來最快。
另外,在 Linux 上建置的二進位檔 只能在相同或更新的 Linux 上執行。在 Ubuntu 20.04 做出來的東西在 20.04 以後可以跑,但在 18.04 不能跑。建置環境的選法直接就決定了可散發的範圍。
flowchart TB
accTitle: publish 前的關卡與可散發範圍
accDescr: Native AOT 的 publish 需要原生工具鏈,沒有的話會在最後的原生連結失敗,而且在 Linux 上建置的二進位檔只能在相同或更新的 Linux 上執行,建置環境的選法就決定了可散發範圍的圖。
tool{"有沒有工具鏈"}
tool -->|"沒有"| fail["在原生連結階段失敗"]
tool -->|"有"| bin["產出各 RID 的二進位檔"]
bin -.-> range["Linux 只能在相同或更新的環境執行"]
range -.-> pick["建置環境的選法就是散發範圍"]
圖 13: 第一道關卡是工具鏈。在 Linux 上,建置環境的新舊決定了可散發範圍。
6.1. csproj
首先在專案檔裡放入 PublishAot。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
範例用 net8.0 就夠了。想法本身在 .NET 9 / 10 也幾乎一樣。
重要的是,與其只在 dotnet publish 的命令列上臨時加,不如
平常就把它放在專案裡,日常就看得到 build / publish 時的分析。
另外,就算放了 <PublishAot>true</PublishAot>,平常在本機執行也不會突然變成 Native AOT。
日常的 dotnet run 與一般執行都是 JIT,Native AOT 編譯的正戲是在 publish 時。
flowchart TB
accTitle: 放入 PublishAot 之後的日常
accDescr: 就算把 PublishAot 放進專案,日常的 dotnet run 仍然是 JIT,Native AOT 編譯的正戲是在 publish 時跑,所以平常就把設定放在專案裡並日常查看 build 與 publish 時的分析很重要的圖。
put["在 csproj 放入 PublishAot"] --> daily["日常的 dotnet run 仍是 JIT"]
put --> pub["publish 時進行 AOT 編譯"]
daily -.-> watch["日常查看分析與 warning"]
圖 14: 日常是 JIT,正戲在 publish。正因如此,設定不要臨時加,而要常駐並持續查看分析。
6.2. publish
例如要給 Windows x64 用,會是這樣。
dotnet publish -c Release -r win-x64
要給 Linux x64 用的話,是這樣。
dotnet publish -c Release -r linux-x64
輸出是固定 RID 的。 看法會從「一支到哪裡都能跑的 .NET DLL」,變成「為那個 OS / 架構做出來的執行檔」。
如果是從 Web API 那一邊入手,從以 Native AOT 為前提的範本開始比較輕鬆。
dotnet new webapiaot -o MyFirstAotWebApi
worker 的話,是這個。
dotnet new worker -o WorkerWithAot --aot
6.3. JSON 的寫法
在 Native AOT 上不起眼卻很常撞到的是 JSON。
System.Text.Json 用平常的感覺去用容易偏向反射,所以改用 source generation 會比較平穩。
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
public sealed class AppConfig
{
public string? Name { get; init; }
public int RetryCount { get; init; }
}
var config = new AppConfig
{
Name = "sample",
RetryCount = 3
};
string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);
實務上,與其記成「支援 Native AOT」,不如記成 往「不要讓執行階段去找型別」的方向靠攏,比較不容易偏掉。
7. 適合的情境
Native AOT 用起來特別順手的,是這幾種情境。
- 以啟動為主角的 CLI / 工具
- 在容器裡大量部署的小型 API
- worker / 背景服務
- 無伺服器或短命處理程序
- 插進原生應用程式裡的小型 .NET 元件
- 不想要求執行環境事先安裝 .NET 執行階段的場合
共通點是 邊界比較清楚,動態機制也比較容易減少。
8. 不適合的情境
反過來,也有一開始就不該把 Native AOT 當主戰場的明確場合。
- WPF / WinForms 的既有大型應用程式本體
- 以 built-in COM interop 為前提的結構
- 以執行階段載入 plugin 為主角的應用程式
- 重度依賴以 reflection 尋找型別的框架的結構
- 把
System.Reflection.Emit或動態 proxy 當成理所當然在用的函式庫 - 中間夾了 C++/CLI 的設計
這些情況下,以 JIT 為前提的一般 .NET、ReadyToRun,或是重新檢視設計上的職責劃分,方向會比較對。
9. 常見的卡關處
最後整理一下 Native AOT 初次上手容易踩到的點。
- 輕看 publish warning
- 如同前述,Native AOT 的 warning 是要認真讀的對象。
- build 過得了,publish 卻壞掉
- publish 時會連相依函式庫都納入認真分析,所以有些問題到這裡才第一次看得見。
- 用同樣的心情看待 ReadyToRun 和 Native AOT
- 名字相近,但限制的強度差很多。
- 一開始就從桌面應用程式本體下手
- 先從主控台 / worker / 小型 Web API 開始會比較平穩。
- JSON 或設定繫結用平常的感覺寫
- 以反射為前提的寫法,之後才會顯現出來。
- 以為和平台無關就直接散發
- Native AOT 的散發產物是固定 RID 的。
- 以為「Native AOT 就是什麼都變快」
- 主角是啟動、散發與執行環境。這裡搞錯,期待值就會偏掉。
在 Native AOT,最終決定可不可行的不是 dotnet build 而是 dotnet publish。
早一點開始跑這一段,後半就比較不會卡住。
flowchart TB
accTitle: 決定可不可行的是 publish
accDescr: build 過得了卻在 publish 壞掉,是因為 publish 時會連相依函式庫都納入認真分析,Native AOT 最終決定可不可行的不是 dotnet build 而是 dotnet publish 的圖。
buildok["dotnet build 過得了"] --> notyet["可不可行還沒決定"]
pubx["dotnet publish"] --> deep["連相依項目一起認真分析"]
deep --> verdict["到這裡才第一次看得出可不可行"]
verdict -.-> early["所以要早一點跑 publish"]
圖 15: 卡關處的共通點。不要因為「build 過了」就放心,要早一點跑 publish。
10. 總結
Native AOT 用一句話講,就是 把 .NET 應用程式從動態的執行模型,往靜態上容易確定的散發模型靠攏的機制。
想先掌握的重點,就這五個。
- Native AOT 在 publish 時把程式事前編譯成原生程式碼
- 對啟動、記憶體與散發上的需求很有效
- 代價是對 reflection、動態產生程式碼、built-in COM、不支援 trimming 的程式碼很嚴苛
- 第一個對象,比起桌面本體,主控台 / worker / 小型 Web API 會平穩得多
- 一邊消掉 warning,一邊以 publish 為基準早一點確認很重要
Native AOT 不是所有 .NET 應用程式都要打開的標準開關。 不過在 啟動很重要、想讓散發變輕、想減少對執行環境的前提要求 的場合,它是相當強的武器。
反過來,在 WPF / WinForms / COM 味道濃厚的世界裡,一般的 .NET 方向比較對的情況仍然很多。 能分辨這兩邊之後,Native AOT 就不再是「難搞的新功能」,而是使用時機清楚的選項。
11. 參考資料
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET(繁體中文)
- Introduction to AOT warnings - .NET
- Fixing trim warnings - .NET
- DynamicallyAccessedMembersAttribute Class - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- 相關文章:從 C/C++ 呼叫 C# Native AOT DLL 的方法
- 相關文章:從 C# 呼叫原生 DLL:C++/CLI 包裝層 vs P/Invoke
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
多執行緒的實務最佳實踐 .NET篇 ── 在增加執行緒之前先決定的事
針對 .NET/C# 整理能防止「建立執行緒後偶爾當機、卡死」的設計定石。內容涵蓋不自行建立執行緒而改用 Task、減少共享可變狀態、鎖的紀律、以 CancellationToken 設計停止機制,以及 UI 執行緒的處理方式。
業務系統的代碼設計 ── 商品代碼・客戶代碼的訂定方式與檢查碼
商品代碼・客戶代碼等業務系統代碼體系的實務指南。有意義代碼與無意義流水號的判斷表、JAN・Luhn等檢查碼算式與C#實作、Excel的0消失對策,乃至位數溢位與遷移一次整理清楚。
如何理解 Windows 的工作階段隔離 ── Session 0・RDP・多使用者同時執行
整理 Windows 應用程式開發者容易混淆的「工作階段」概念。說明服務無法顯示 UI 的 Session 0 隔離原因、RDP 連線時工作階段的行為、具名物件的工作階段隔離,以及共用 PC・RDS 環境常見的設計失誤,從實務角度解說。
Windows 的行程間通訊該怎麼選 ── 具名管道 / TCP / gRPC / 共享記憶體 / COM 判斷表
整理 Windows 應用程式之間該如何選擇溝通方式。以判斷表整理具名管道、本機 TCP、gRPC、共享記憶體、檔案協作、COM 各自的強項與陷阱,並從實務角度說明 UI+服務分離・32bit/64bit 橋接・權限邊界等典型架構,以及具名管道的實作範例。
Windows應用程式資料儲存位置怎麼選 ── SQLite / JSON / 登錄檔 / Access 判斷表
Windows桌面應用程式的資料該存在哪裡、用什麼格式儲存?本文整理AppData/ProgramData的使用區分,以及SQLite、JSON檔案、登錄檔、Access(.accdb)各自的優勢與陷阱,並附判斷表,從實務角度說明防止資料損毀與位元數問題等注意事項。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
32 位元 / 64 位元互通
整理 32 位元 / 64 位元互通、原生邊界與相關 Windows 設計判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- Native AOT 是什麼?
- 它是一種發行方式:在 publish 時就把 .NET 應用程式事前編譯成原生程式碼再散發。因為不使用執行階段 JIT,啟動時間與記憶體占用容易變好,也比較容易散發到沒有安裝 .NET 執行階段的環境。代價是它和自由的反射、動態產生程式碼、built-in COM,以及不支援 trimming 的函式庫都合不來。與其說是變快的魔法,不如說它是為了啟動、散發與執行環境的需求,捨掉一部分動態世界而往靜態世界靠攏的發行模型。
- Native AOT 和 ReadyToRun 有什麼不同?
- ReadyToRun 是保留 IL,只把 JIT 的工作稍微提前的方式,執行階段仍有會用到 JIT 的場合。它的相容性廣,動態功能大致上也還很好用。Native AOT 則不以執行階段 JIT 本身為前提,散發產物以原生執行檔為主,啟動相當容易改善,代價是限制變強。即使名稱裡都帶著 AOT,ReadyToRun 走的是「讓 JIT 輕鬆一點」,Native AOT 走的是「不以 JIT 為前提」,兩者的認真程度差很多。
- WPF 或 WinForms 的應用程式可以使用 Native AOT 嗎?
- 現階段最好相當謹慎看待。Windows 的 Native AOT 沒有 built-in COM,WPF 和 trimming 合不來,WinForms 又重度依賴 built-in COM marshalling,兩者都不適合當第一個 Native AOT 候選。如果需要 COM,維持 JIT,或是以 ComWrappers / source-generated COM 為前提重新設計,有些情況下方向會比較對。當作入口的話,主控台程式、worker、小型 Web API 會直接了當得多。
- Native AOT 適合什麼樣的應用程式?
- 適合以啟動為主角的 CLI 工具、在容器裡大量部署的小型 API、worker 與背景服務、無伺服器或短命處理程序、插進原生應用程式裡的小型 .NET 元件,以及不想要求執行環境事先安裝 .NET 執行階段的場合。這些情境的共同點是邊界比較清楚,動態機制也比較容易減少。反過來說,以執行階段載入外掛為主角的應用程式,或是重度依賴以反射尋找型別的框架的結構,就不適合。