.NET NativeAOT 用法指南
隨著 .NET 8 的發(fā)布,一種新的“時(shí)尚”應(yīng)用模型 NativeAOT 開(kāi)始在各種真實(shí)世界的應(yīng)用中廣泛使用。
除了對(duì) NativeAOT 工具鏈的基本使用外,“NativeAOT”一詞還帶有原生世界的所有限制,因此您必須知道如何處理這些問(wèn)題才能正確使用它。
在這篇博客中,我將討論它們。
基本用法
使用 NativeAOT 非常簡(jiǎn)單,只需要在發(fā)布應(yīng)用時(shí)使用 MSBuild 傳遞一個(gè)屬性 PublishAot=true
即可。
通常,它可以是:
dotnet publish -c Release -r win-x64 /p:PublishAot=true
其中 win-x64
是運(yùn)行時(shí)標(biāo)識(shí)符,可以替換為 linux-x64
,osx-arm64
或其他平臺(tái)。您必須指定它,因?yàn)?NativeAOT 需要為您指定的運(yùn)行時(shí)標(biāo)識(shí)符生成原生代碼。
然后發(fā)布的應(yīng)用可以在 bin/Release/<target framework>/<runtime identifier>/publish
中找到
關(guān)于編譯
在討論使用 NativeAOT 時(shí)可能遇到的各種問(wèn)題的解決方案之前,我們需要稍微深入一點(diǎn),看看 NativeAOT 是如何編譯代碼的。
我們經(jīng)常聽(tīng)說(shuō) NativeAOT 會(huì)剪裁掉沒(méi)有被使用的代碼。而實(shí)際上,它并不像 IL 剪裁那樣從程序集中剪裁掉不必要的代碼,而是只編譯代碼中引用的東西。
NativeAOT 編譯包括兩個(gè)階段:
- 掃描 IL 代碼,構(gòu)建整個(gè)程序視圖(一個(gè)依賴圖),其中包含所有需要編譯的必要依賴節(jié)點(diǎn)。
- 對(duì)依賴圖中的每個(gè)方法進(jìn)行實(shí)際的編譯,生成代碼。
請(qǐng)注意,在編譯過(guò)程中可能會(huì)出現(xiàn)一些“延遲”的依賴,因此上述兩個(gè)階段可能會(huì)交錯(cuò)出現(xiàn)。
這意味著,在分析過(guò)程中沒(méi)有被計(jì)算為依賴的任何東西最終都不會(huì)被編譯。
反射
依賴圖是在編譯期間靜態(tài)構(gòu)建的,這也意味著任何無(wú)法靜態(tài)分析的東西都不會(huì)被編譯。不幸的是,反射,即在不事先告訴編譯器的情況下在運(yùn)行時(shí)獲取東西,正是編譯器無(wú)法弄清楚的一件事。
NativeAOT 編譯器有一些能力可以根據(jù)編譯時(shí)的字面量來(lái)推斷出反射調(diào)用需要什么東西。
例如:
var type = Type.GetType("Foo"); Activator.CreateInstance(type); class Foo { public Foo() => Console.WriteLine("Foo instantiated"); }
上面的反射目標(biāo)(即 Foo
)可以被編譯器弄清楚,因?yàn)榫幾g器可以看到你試圖獲取類型 Foo
,所以類型 Foo
會(huì)被標(biāo)記為一個(gè)依賴,這導(dǎo)致 Foo
被編譯到最終的產(chǎn)物中。
如果你運(yùn)行這個(gè)程序,它會(huì)如預(yù)期地打印 Foo instantiated
。
但是如果我們將代碼改為如下:
var type = Type.GetType(Console.ReadLine()); Activator.CreateInstance(type); class Foo { public Foo() => Console.WriteLine("Foo instantiated"); }
現(xiàn)在讓我們用 NativeAOT 構(gòu)建并運(yùn)行這個(gè)程序,然后輸入 Foo
來(lái)創(chuàng)建一個(gè) Foo
的實(shí)例。你會(huì)立刻得到一個(gè)異常:
Unhandled Exception: System.ArgumentNullException: Value cannot be null. (Parameter 'type') at System.ArgumentNullException.Throw(String) + 0x2b at System.ActivatorImplementation.CreateInstance(Type, Boolean) + 0xe7 ...
這是因?yàn)榫幾g器無(wú)法看到你在哪里使用了 Foo
,所以它根本不會(huì)為 Foo
生成任何代碼,導(dǎo)致這里的 type
為 null
。
此外,依賴分析是精確到單個(gè)方法的,這意味著即使一個(gè)類型被認(rèn)為是一個(gè)依賴,如果該類型中的任何方法沒(méi)有被使用,該方法也不會(huì)被包含在代碼生成中。
雖然這可以通過(guò)將所有類型和方法添加到依賴圖中來(lái)解決,這樣編譯器就會(huì)為它們生成代碼。這就是 TrimmerRootAssembly
的作用:通過(guò)提供 TrimmerRootAssembly
,NativeAOT 編譯器會(huì)將你指定的程序集中的所有東西都作為根。
但是涉及泛型的情況就不是這樣了。
動(dòng)態(tài)泛型實(shí)例化
在 .NET 中,我們有泛型,編譯器會(huì)為每個(gè)非共享的泛型類型和方法生成不同的代碼。
假設(shè)我們有一個(gè)類型 Point<T>
:
struct Point<T> { public T X, Y; }
如果我們有一段代碼試圖使用 Point<int>
,編譯器會(huì)為 Point<int>
生成專門(mén)的代碼,使得 Point.X
和 Point.Y
都是 int
。如果我們有一個(gè) Point<float>
,編譯器會(huì)生成另一個(gè)專門(mén)的代碼,使得 Point.X
和 Point.Y
都是 float
。
通常情況下,這不會(huì)導(dǎo)致任何問(wèn)題,因?yàn)榫幾g器可以靜態(tài)地找出你在代碼中使用的所有實(shí)例化,直到你試圖使用反射來(lái)構(gòu)造一個(gè)泛型類型或一個(gè)泛型方法:
var type = Type.GetType(Console.ReadLine()); var pointType = typeof(Point<>).MakeGenericType(type);
上面的代碼在 NativeAOT 下不會(huì)工作,因?yàn)榫幾g器無(wú)法推斷出 Point<T>
的實(shí)例化,所以編譯器既不會(huì)生成 Point<int>
的代碼,也不會(huì)生成 Point<float>
的代碼。
盡管編譯器可以為 int
,float
,甚至泛型類型定義 Point<>
生成代碼,但是如果編譯器沒(méi)有生成 Point<int>
的實(shí)例化代碼,你就無(wú)法使用 Point<int>
。
即使你使用 TrimmerRootAssembly
來(lái)告訴編譯器將你的程序集中的所有東西都作為根,也仍然不會(huì)為像 Point<int>
或 Point<float>
這樣的實(shí)例化生成代碼,因?yàn)樗鼈冃枰鶕?jù)類型參數(shù)來(lái)單獨(dú)構(gòu)造。
解決方案
既然我們已經(jīng)找出了在 NativeAOT 下可能發(fā)生的潛在問(wèn)題,讓我們來(lái)談?wù)劷鉀Q方案。
在其他地方使用它
最簡(jiǎn)單的想法是,我們可以通過(guò)在代碼中使用它來(lái)讓編譯器知道我們需要什么。
例如,對(duì)于代碼
var type = Type.GetType(Console.ReadLine()); var pointType = typeof(Point<>).MakeGenericType(type);
只要我們知道我們要使用 Point<int>
和 Point<float>
,我們可以在其他地方使用它一次,然后編譯器就會(huì)為它們生成代碼:
// 我們使用一個(gè)永遠(yuǎn)為假的條件來(lái)確保代碼不會(huì)被執(zhí)行 // 因?yàn)槲覀冎幌胱尵幾g器知道依賴關(guān)系 // 注意,如果我們?cè)谶@里簡(jiǎn)單地使用一個(gè) `if (false)` // 這個(gè)分支會(huì)被編譯器完全移除,因?yàn)樗嵌嘤嗟? // 所以,讓我們?cè)谶@里使用一個(gè)不平凡但不可能的條件 if (DateTime.Now.Year < 0) { var list = new List<Type>(); list.Add(typeof(Point<int>)); list.Add(typeof(Point<float>)); }
DynamicDependency
我們有一個(gè)屬性 DynamicDependencyAttribute
來(lái)告訴編譯器一個(gè)方法依賴于另一個(gè)類型或方法。
所以我們可以利用它來(lái)告訴編譯器:“如果 A 被包含在依賴圖中,那么也添加 B”。
下面是一個(gè)例子:
class Foo { readonly Type t = typeof(Bar); [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))] public void A() { foreach (var prop in t.GetProperties()) { Console.WriteLine(prop); } } } class Bar { public int X { get; set; } public int Y { get; set; } }
現(xiàn)在只要編譯器發(fā)現(xiàn)有任何代碼路徑調(diào)用了 Foo.A
,Bar
中的所有公共屬性都會(huì)被添加到依賴圖中,這樣我們就能夠?qū)?nbsp;Bar
的每個(gè)公共屬性進(jìn)行動(dòng)態(tài)反射調(diào)用。
這個(gè)屬性還有許多重載,可以接受不同的參數(shù)來(lái)適應(yīng)不同的用例,您可以在這里查看文檔。
此外,現(xiàn)在我們知道 Foo.A
中的動(dòng)態(tài)反射在剪裁和 NativeAOT 下不會(huì)造成任何問(wèn)題,我們可以使用 UnconditionalSuppressMessage
來(lái)抑制警告信息,這樣在構(gòu)建過(guò)程中就不會(huì)再產(chǎn)生任何警告了。
class Foo { readonly Type t = typeof(Bar); [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Bar))] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2080", Justification = "The properties of Bar have been preserved by DynamicDependency.")] public void A() { foreach (var prop in t.GetProperties()) { Console.WriteLine(prop); } } }
DynamicallyAccessedMembers
有時(shí)我們?cè)噲D動(dòng)態(tài)地訪問(wèn)類型 T
的成員,其中 T
可以是一個(gè)類型參數(shù)或一個(gè) Type
的實(shí)例:
void Foo<T>() { foreach (var prop in typeof(T).GetProperties()) { Console.WriteLine(prop); } } class Bar { public int X { get; set; } public int Y { get; set; } }
如果我們調(diào)用 Foo<Bar>
,很不幸,這在 NativeAOT 下不會(huì)工作。編譯器確實(shí)看到你是用類型參數(shù) Bar
調(diào)用 Foo
的,但在 Foo<T>
的上下文中,編譯器不知道 T
是什么,而且沒(méi)有其他代碼直接使用 Bar
的屬性,所以編譯器不會(huì)為 Bar
的屬性生成代碼。
這里我們可以使用 DynamicallyAccessedMembers
來(lái)告訴編譯器為 T
的所有公共屬性生成代碼:
void Foo<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>() { // ... }
現(xiàn)在當(dāng)編譯器編譯調(diào)用 Foo<Bar>
時(shí),它知道 T
(特別的,這里指 Bar
)的所有公共屬性都應(yīng)該被視為依賴。
這個(gè)屬性也可以應(yīng)用在一個(gè) Type
上:
Foo(typeof(Bar)); void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t) { foreach (var prop in t.GetProperties()) { Console.WriteLine(prop); } }
甚至在一個(gè) string
上:
Foo("Bar"); void Foo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] string s) { foreach (var prop in Type.GetType(s).GetProperties()) { Console.WriteLine(prop); } }
所以在這里你可能會(huì)發(fā)現(xiàn)我們有一個(gè)替代方案,用于我們?cè)?nbsp;DynamicDependency
一節(jié)中提到的代碼示例:
class Foo { [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] readonly Type t = typeof(Bar); public void A() { foreach (var prop in t.GetProperties()) { Console.WriteLine(prop); } } }
順便說(shuō)一句,這也是推薦的方法。
TrimmerRootAssembly
如果你不擁有代碼,但你仍然希望代碼在 NativeAOT 下工作。你可以嘗試使用 TrimmerRootAssembly
來(lái)告訴編譯器將一個(gè)程序集中的所有類型和方法都作為依賴。但請(qǐng)注意,這種方法不適用于泛型實(shí)例化。
<ItemGroup> <TrimmerRootAssembly Include="MyAssembly" /> </ItemGroup>
TrimmerRootDescriptor
對(duì)于高級(jí)用戶,他們可能想要控制從一個(gè)程序集中包含什么。在這種情況下,可以指定一個(gè) TrimmerRootDescriptor
:
<ItemGroup> <TrimmerRootDescriptor Include="link.xml" /> </ItemGroup>
TrimmerRootDescriptor 文件的文檔和格式可以在這里找到。
Runtime Directives
對(duì)于泛型實(shí)例化的情況,它們無(wú)法通過(guò) TrimmerRootAssembly 或 TrimmerRootDescriptor 來(lái)解決,這里需要一個(gè)包含 runtime directives 的文件來(lái)告訴編譯器需要編譯的東西。
<ItemGroup> <RdXmlFile Include="rd.xml" /> </ItemGroup>
在 rd.xml
中,你可以為你的泛型類型和方法指定實(shí)例化。
rd.xml
文件的文檔和格式可以在這里找到。
這種方法不推薦,但它可以解決你在使用 NativeAOT 時(shí)遇到的一些難題。請(qǐng)?jiān)谑褂?trimmer descriptor 或 runtime directives 之前,總是考慮用 DynamicallyAccessedMembers
和 DynamicDependency
來(lái)注釋你的代碼,使其與剪裁/AOT 兼容。
結(jié)語(yǔ)
NativeAOT 是 .NET 中一個(gè)非常棒和強(qiáng)大的工具。有了 NativeAOT,你可以以可預(yù)測(cè)的性能構(gòu)建你的應(yīng)用,同時(shí)節(jié)省資源(更低的內(nèi)存占用和更小的二進(jìn)制大小)。
它還將 .NET 帶到了不允許 JIT 編譯器的平臺(tái),例如 iOS 和主機(jī)平臺(tái)。此外,它還使 .NET 能夠運(yùn)行在嵌入式設(shè)備甚至裸機(jī)設(shè)備上(例如在 UEFI 上運(yùn)行)。
在使用工具之前了解工具,這樣你會(huì)節(jié)省很多時(shí)間。
到此這篇關(guān)于.NET NativeAOT 指南的文章就介紹到這了,更多相關(guān).NET NativeAOT 指南內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
DataGridView中綁定DataTable數(shù)據(jù)及相關(guān)操作實(shí)現(xiàn)代碼
DataGridView中綁定DataTable數(shù)據(jù)及相關(guān)操作2010-02-02.Net?Core?NPOI?導(dǎo)出多級(jí)表頭的實(shí)現(xiàn)代碼
這篇文章介紹了如何使用.NetCore和NPOI庫(kù)導(dǎo)出多級(jí)表頭的表格數(shù)據(jù),并附上了源碼,感興趣的朋友一起看看吧2024-11-11ASP.NET中實(shí)現(xiàn)把Json數(shù)據(jù)轉(zhuǎn)換為ADO.NET DataSet對(duì)象
這篇文章主要介紹了ASP.NET中實(shí)現(xiàn)把Json數(shù)據(jù)轉(zhuǎn)換為ADO.NET DataSet對(duì)象,本文講解設(shè)計(jì)及實(shí)現(xiàn)方法,相關(guān)代碼托管到GITHUB,需要的朋友可以參考下2015-03-03asp.net Page.EnableEventValidation 屬性驗(yàn)證服務(wù)器控件的回發(fā)和回調(diào)事件出現(xiàn)的錯(cuò)誤
Page.EnableEventValidation 屬性驗(yàn)證服務(wù)器控件的回發(fā)和回調(diào)事件出現(xiàn)的錯(cuò)誤前兩天用jQuery做了一個(gè)包含DropDownList聯(lián)動(dòng)的頁(yè)面,數(shù)據(jù)通過(guò)Ajax請(qǐng)求得到的。2010-10-10簡(jiǎn)單使用BackgroundWorker創(chuàng)建多個(gè)線程的教程
簡(jiǎn)單使用BackgroundWorker創(chuàng)建多個(gè)線程的教程,需要的朋友可以參考一下2013-03-03asp.net Mvc4 使用ajax結(jié)合分頁(yè)插件實(shí)現(xiàn)無(wú)刷新分頁(yè)
本篇文章主要介紹了 asp.net Mvc4 使用ajax結(jié)合分頁(yè)插件實(shí)現(xiàn)無(wú)刷新分頁(yè),ajax通過(guò)回調(diào)函數(shù)把控制器返回的分部視圖內(nèi)容加載到主視圖中顯示,有興趣的可以了解一下。2017-01-01