.NET NativeAOT 用法指南
隨著 .NET 8 的發(fā)布,一種新的“時(shí)尚”應(yīng)用模型 NativeAOT 開始在各種真實(shí)世界的應(yīng)用中廣泛使用。
除了對(duì) NativeAOT 工具鏈的基本使用外,“NativeAOT”一詞還帶有原生世界的所有限制,因此您必須知道如何處理這些問題才能正確使用它。
在這篇博客中,我將討論它們。
基本用法
使用 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í)可能遇到的各種問題的解決方案之前,我們需要稍微深入一點(diǎn),看看 NativeAOT 是如何編譯代碼的。
我們經(jīng)常聽說 NativeAOT 會(huì)剪裁掉沒有被使用的代碼。而實(shí)際上,它并不像 IL 剪裁那樣從程序集中剪裁掉不必要的代碼,而是只編譯代碼中引用的東西。
NativeAOT 編譯包括兩個(gè)階段:
- 掃描 IL 代碼,構(gòu)建整個(gè)程序視圖(一個(gè)依賴圖),其中包含所有需要編譯的必要依賴節(jié)點(diǎn)。
- 對(duì)依賴圖中的每個(gè)方法進(jìn)行實(shí)際的編譯,生成代碼。
請(qǐng)注意,在編譯過程中可能會(huì)出現(xiàn)一些“延遲”的依賴,因此上述兩個(gè)階段可能會(huì)交錯(cuò)出現(xiàn)。
這意味著,在分析過程中沒有被計(jì)算為依賴的任何東西最終都不會(huì)被編譯。
反射
依賴圖是在編譯期間靜態(tài)構(gòu)建的,這也意味著任何無法靜態(tài)分析的東西都不會(huì)被編譯。不幸的是,反射,即在不事先告訴編譯器的情況下在運(yùn)行時(shí)獲取東西,正是編譯器無法弄清楚的一件事。
NativeAOT 編譯器有一些能力可以根據(jù)編譯時(shí)的字面量來推斷出反射調(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 來創(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器無法看到你在哪里使用了 Foo,所以它根本不會(huì)為 Foo 生成任何代碼,導(dǎo)致這里的 type 為 null。
此外,依賴分析是精確到單個(gè)方法的,這意味著即使一個(gè)類型被認(rèn)為是一個(gè)依賴,如果該類型中的任何方法沒有被使用,該方法也不會(huì)被包含在代碼生成中。
雖然這可以通過將所有類型和方法添加到依賴圖中來解決,這樣編譯器就會(huì)為它們生成代碼。這就是 TrimmerRootAssembly 的作用:通過提供 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> 生成專門的代碼,使得 Point.X 和 Point.Y 都是 int。如果我們有一個(gè) Point<float>,編譯器會(huì)生成另一個(gè)專門的代碼,使得 Point.X 和 Point.Y 都是 float。
通常情況下,這不會(huì)導(dǎo)致任何問題,因?yàn)榫幾g器可以靜態(tài)地找出你在代碼中使用的所有實(shí)例化,直到你試圖使用反射來構(gòu)造一個(gè)泛型類型或一個(gè)泛型方法:
var type = Type.GetType(Console.ReadLine()); var pointType = typeof(Point<>).MakeGenericType(type);
上面的代碼在 NativeAOT 下不會(huì)工作,因?yàn)榫幾g器無法推斷出 Point<T> 的實(shí)例化,所以編譯器既不會(huì)生成 Point<int> 的代碼,也不會(huì)生成 Point<float> 的代碼。
盡管編譯器可以為 int,float,甚至泛型類型定義 Point<> 生成代碼,但是如果編譯器沒有生成 Point<int> 的實(shí)例化代碼,你就無法使用 Point<int>。
即使你使用 TrimmerRootAssembly 來告訴編譯器將你的程序集中的所有東西都作為根,也仍然不會(huì)為像 Point<int> 或 Point<float> 這樣的實(shí)例化生成代碼,因?yàn)樗鼈冃枰鶕?jù)類型參數(shù)來單獨(dú)構(gòu)造。
解決方案
既然我們已經(jīng)找出了在 NativeAOT 下可能發(fā)生的潛在問題,讓我們來談?wù)劷鉀Q方案。
在其他地方使用它
最簡(jiǎn)單的想法是,我們可以通過在代碼中使用它來讓編譯器知道我們需要什么。
例如,對(duì)于代碼
var type = Type.GetType(Console.ReadLine()); var pointType = typeof(Point<>).MakeGenericType(type);
只要我們知道我們要使用 Point<int> 和 Point<float>,我們可以在其他地方使用它一次,然后編譯器就會(huì)為它們生成代碼:
// 我們使用一個(gè)永遠(yuǎn)為假的條件來確保代碼不會(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 來告訴編譯器一個(gè)方法依賴于另一個(gè)類型或方法。
所以我們可以利用它來告訴編譯器:“如果 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ù)來適應(yīng)不同的用例,您可以在這里查看文檔。
此外,現(xiàn)在我們知道 Foo.A 中的動(dòng)態(tài)反射在剪裁和 NativeAOT 下不會(huì)造成任何問題,我們可以使用 UnconditionalSuppressMessage 來抑制警告信息,這樣在構(gòu)建過程中就不會(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)地訪問類型 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 是什么,而且沒有其他代碼直接使用 Bar 的屬性,所以編譯器不會(huì)為 Bar 的屬性生成代碼。
這里我們可以使用 DynamicallyAccessedMembers 來告訴編譯器為 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);
}
}
}順便說一句,這也是推薦的方法。
TrimmerRootAssembly
如果你不擁有代碼,但你仍然希望代碼在 NativeAOT 下工作。你可以嘗試使用 TrimmerRootAssembly 來告訴編譯器將一個(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í)例化的情況,它們無法通過 TrimmerRootAssembly 或 TrimmerRootDescriptor 來解決,這里需要一個(gè)包含 runtime directives 的文件來告訴編譯器需要編譯的東西。
<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>在 rd.xml 中,你可以為你的泛型類型和方法指定實(shí)例化。
rd.xml 文件的文檔和格式可以在這里找到。
這種方法不推薦,但它可以解決你在使用 NativeAOT 時(shí)遇到的一些難題。請(qǐng)?jiān)谑褂?trimmer descriptor 或 runtime directives 之前,總是考慮用 DynamicallyAccessedMembers 和 DynamicDependency 來注釋你的代碼,使其與剪裁/AOT 兼容。
結(jié)語
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庫導(dǎo)出多級(jí)表頭的表格數(shù)據(jù),并附上了源碼,感興趣的朋友一起看看吧2024-11-11
ASP.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-03
asp.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)的頁面,數(shù)據(jù)通過Ajax請(qǐng)求得到的。2010-10-10
簡(jiǎn)單使用BackgroundWorker創(chuàng)建多個(gè)線程的教程
簡(jiǎn)單使用BackgroundWorker創(chuàng)建多個(gè)線程的教程,需要的朋友可以參考一下2013-03-03
asp.net Mvc4 使用ajax結(jié)合分頁插件實(shí)現(xiàn)無刷新分頁
本篇文章主要介紹了 asp.net Mvc4 使用ajax結(jié)合分頁插件實(shí)現(xiàn)無刷新分頁,ajax通過回調(diào)函數(shù)把控制器返回的分部視圖內(nèi)容加載到主視圖中顯示,有興趣的可以了解一下。2017-01-01

