.NET多平臺應用程式UI (MAUI)將android、iOS、macOS和Windows API統一為一個API,這樣你就可以編寫一個應用程式在許多平臺上本機執行。我們專注於提高您的日常生產力以及您的應用程式的效能。我們認為,開發人員生產率的提高不應該以應用程式效能為代價。
應用程式的大小也是如此——在一個空白的.NET MAUI應用程式中存在什麼開銷?當我們開始優化.NET MAUI時,很明顯iOS需要做一些工作來改善應用程式的大小,而android則缺乏啟動效能。
一個dotnet new maui專案的iOS應用程式最初大約是18MB。同樣,在之前的預覽中.NET MAUI在android上的啟動時間也不是很理想:
.NET Podcast:
https://github.com/microsoft/...
這是在Pixel 5裝置上平均執行10次得到的結果。有關這些數字是如何獲得的,請參閱我們的maui-profiling檔案。
我們的目標是讓.NET MAUI比它的前身Xamarin更快。很明顯,我們在.NET MAUI本身也有一些工作要做。dotnet new android 模板的釋出速度已經超過Xamarin.Android,主要是因為.NET 6中新的BCL和Mono執行時。
新的.NET maui模板還沒有使用Shell導航模式,但是計劃將其作為.NET maui的預設導航模式。當我們採用這個更改時,我們知道會對模板中的效能造成影響。
幾個不同團隊的合作才有了今天的成就。我們改進了Microsoft.Extensions ,依賴注入的使用,AOT編譯,Java互操作,XAML,.NET MAUI程式碼,等等方面。
.NET Podcast App (Shell):https://github.com/microsoft/...
**這是原始的dotnet new maui模板,沒有使用Shell。
內容十分豐富,來看是否有您期待的更新吧!
主要內容
啟動效能的改進
- 在移動裝置上進行分析
- 測量隨著時間的推移
- Profiled AOT
- 單檔案程式集儲存器
- Spanify.RegisterNativeMembers
- System.Reflection.Emit和建構函式
- System.Reflection.Emit和方法
- 更新的Java.Interop APIs
- 多維Java陣列
- 為android影像使用Glide
- 減少Java互操作呼叫
- 將android XML移植到Java
- 刪除Microsoft.Extensions.Hosting
- 在啟動時減少Shell初始化
- 字型不應該使用臨時檔案
- 編譯時在平臺上計算
- 在XAML中使用編譯轉換器
- 優化顏色解析
- 不要使用區域性識別的字串比較
- 懶惰地建立日誌
- 使用工廠方法進行依賴注入
- 懶惰地負載ConfigurationManager
- 預設VerifyDependencyInjectionOpenGenericServiceTrimmability
- 改進內建AOT配置檔案
- 啟用AOT影像的延遲載入
- 刪除System.Uri中未使用的編碼物件
應用程式大小的改進
.NET Podcast示例中的改進
實驗性或高階選項
啟動效能的改進
▌在移動裝置上進行分析
我必須提到移動平臺上可用的.NET診斷工具,因為它是我們使.NET MAUI更快的第0步。
分析.NET 6 android應用程式需要使用一個叫做dotnet-dsrouter的工具。該工具使dotnet跟蹤連線到一個執行的移動應用程式在android, iOS等。這可能是我們用來分析.NET MAUI的最有影響力的工具。
要開始使用dotnet trace和dsrouter,首先通過adb配置一些設定並啟動dsrouter:
adb reverse tcp:9000 tcp:9001
adb shell setprop debug.mono.profile '127.0.0.1:9000,suspend'
dotnet-dsrouter client-server -tcps 127.0.0.1:9001 -ipcc /tmp/maui-app --verbose debug
下一步啟動dotnet跟蹤,如:
dotnet-trace collect --diagnostic-port /tmp/maui-app --format speedscope
在啟動一個使用-c Release和-p:androidEnableProfiler=true構建的android應用程式後,當dotnet trace輸出時,你會注意到連線:
Press <Enter> or <Ctrl+C> to exit...812 (KB)
在您的應用程式完全啟動後,只需按下enter鍵就可以得到一個儲存在當前目錄的*.speedscope。你可以在https://speedscope.app上開啟這個檔案,深入瞭解每個方法在應用程式啟動期間所花費的時間:
在android應用程式中使用dotnet跟蹤的更多細節,請參閱我們的文件。我建議在android裝置上分析Release版本,以獲得應用在現實世界中的最佳表現。
▌測量隨著時間的推移
我們在.NET基礎團隊的朋友建立了一個管道來跟蹤.NET MAUI效能場景,例如:
- 包大小
- 磁碟大小(未壓縮)
- 單個檔案分類
- 應用程式啟動
隨著時間的推移,這使我們能夠看到改進或迴歸的影響,看到dotnet/maui回購的每個提交的數字。我們還可以確定這種差異是否是由xamarin-android、xamarin-macios或dotnet/runtime中的變化引起的。
例如,在物理Pixel 4a裝置上執行的dotnet new maui模板的啟動時間(以毫秒為單位)圖:
注意,Pixel 4a比Pixel 5要慢得多。
我們可以精確地指出在dotnet/maui中發生的迴歸和改進。這對於追蹤我們的目標是非常有用的。
同樣地,我們可以在相同的Pixel 4a裝置上看到.NET Podcast應用隨著時間的推移所取得的進展:
這張圖表是我們真正關注的焦點,因為它是一款“真正的應用”,接近於開發者在自己的手機應用中看到的內容。
至於應用程式大小,它是一個更穩定的數字——當情況變得更糟或更好時,它很容易歸零:
請參閱dotnet-podcasts#58, Android x# 520和dotnet/maui#6419瞭解這些改進的詳細資訊。
▌異形AOT
在我們對.NET MAUI的初始效能測試中,我們看到了JIT(及時)和AOT(提前)編譯的程式碼是如何執行的:
每次呼叫c#方法時都會發生JIT處理,這會隱式地影響移動應用程式的啟動效能。
另一個問題是AOT導致的應用程式大小增加。每個.NET程式集都會在最終應用中新增一個android本地庫。為了更好地利用這兩個世界,啟動跟蹤或分析AOT是Xamarin.Android當前的一個特性。這是一種AOT應用程式啟動路徑的機制,它顯著提高了啟動時間,而只增加了適度的應用程式大小。
在.NET 6版本中,這是完全有意義的預設選項。在過去,使用Xamarin.Android進行任何型別的AOT都需要Android NDK(下載多個gb)。我們在沒有安裝android NDK的情況下構建了AOT應用程式,使其成為可能。
我們為 dotnet new android, maui,和maui-blazor模板的內建配置檔案,使大多數應用程式受益。如果你想在.NET 6中記錄一個自定義配置檔案,你可以試試我們的實驗性的Mono.Profiler. Android包。我們正在努力在未來的.NET版本中完全支援記錄自定義概要檔案。
檢視xamarin-Android#6547和dotnet/maui#4859瞭解這個改進的細節。
▌單檔案程式集儲存器
之前,如果你在你最喜歡的zip檔案實用程式中檢視Release android .apk內容,你可以看到.NET程式集位於:
assemblies/Java.Interop.dll
assemblies/Mono.android.dll
assemblies/System.Runtime.dll
assemblies/arm64-v8a/System.Private.CoreLib.dll
assemblies/armeabi-v7a/System.Private.CoreLib.dll
assemblies/x86/System.Private.CoreLib.dll
assemblies/x86_64/System.Private.CoreLib.dll
這些檔案是通過mmap系統呼叫單獨載入的,這是應用程式中每個.NET程式集的成本。這是在android工作負載中用C/ c++實現的,使用Mono執行時為程式集載入提供的回撥。MAUI應用程式有很多程式集,所以我們引入了一個新的$(androidUseAssemblyStore)特性,該特性在Release版本中預設啟用。
在這個改變之後,你會得到:
assemblies/assemblies.manifest
assemblies/assemblies.blob
assemblies/assemblies.arm64_v8a.blob
assemblies/assemblies.armeabi_v7a.blob
assemblies/assemblies.x86.blob
assemblies/assemblies.x86_64.blob
現在android啟動只需要呼叫mmap兩次:一次是assemblies.blob,第二次是特定於體系結構的Blob。這對帶有許多. net程式集的應用程式產生了明顯的影響。
如果你需要檢查編譯過的android應用程式中這些程式集的IL,我們建立了一個程式集儲存讀取器工具來“解包”這些檔案。
另一個選擇是在構建應用程式時禁用這些設定:
dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:Android EnableAssemblyCompression=false
這樣你就可以用你喜歡的壓縮工具解壓生成的.apk檔案,並使用ILSpy這樣的工具來檢查.NET程式集。這是一個很好的方法來診斷修剪器/連結器問題。
檢視xamarin-android#6311瞭解關於這個改進的詳細資訊。
▌Spanify RegisterNativeMembers
當用Java建立c#物件時,會呼叫一個小型的Java包裝器,例如:
public class MainActivity extends Android.app.Activity
{
public static final String methods;
static {
methods = "n_onCreate:(LAndroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n";
mono.Android.Runtime.register ("foo.MainActivity, foo", MainActivity.class, methods);
}
方法列表是一個以\n和:分隔的Java本機介面(JNI)簽名列表,這些簽名在託管的c#程式碼中被重寫。對於在c#中重寫的每個Java方法,您都會得到一個這樣的方法。
當實際的Java onCreate()方法被呼叫為一個android活動:
public void onCreate (Android.os.Bundle p0)
{
n_onCreate (p0);
}
private native void n_onCreate (Android.os.Bundle p0);
通過各種各樣的魔術和手勢,n_onCreate呼叫到Mono執行時,並呼叫c#中的OnCreate()方法。
拆分\n和:-分隔的方法列表的程式碼是在Xamarin早期使用string.Split()編寫的。可以說,Span<T>在那時還不存在,但我們現在可以使用它!這提高了任何繼承Java類的c#類的成本,因此這是一個比.NET MAUI更廣泛的改進。
你可能會問,“為什麼要使用字串呢?”使用Java陣列似乎比分隔字串對效能的影響更大。在我們的測試中,呼叫JNI來獲取Java陣列元素,效能比字串差。Split和Span的新用法。對於如何在未來的.NET版本中重新構建它,我們有一些想法。
除了.NET 6之外,針對當前客戶Xamarin. Android的最新版本也附帶了這一更改。
檢視xamarin-android#6708瞭解關於此改進的詳細資訊。
▌System.Reflection.Emit和建構函式
在使用Xamarin的早期,我們有一個從Java呼叫c#建構函式的有點複雜的方法。
首先,我們有一些在啟動時發生的反射呼叫:
static MethodInfo newobject = typeof (System.Runtime.CompilerServices.RuntimeHelpers).GetMethod ("GetUninitializedObject", BindingFlags.Public | BindingFlags.Static)!;
static MethodInfo gettype = typeof (System.Type).GetMethod ("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static)!;
static FieldInfo handle = typeof (Java.Lang.Object).GetField ("handle", BindingFlags.NonPublic | BindingFlags.Instance)!;
這似乎是Mono早期版本遺留下來的,並一直延續到今天。例如,可以直接呼叫RuntimeHelpers.GetUninitializedObject()。
然後是一些複雜的System.Reflection.Emit用法,並在
System.Reflection.ConstructorInfo中傳遞一個cinfo例項:
DynamicMethod method = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), typeof (void), new Type [] {typeof (IntPtr), typeof (object []) }, typeof (DynamicMethodNameCounter), true);
ILGenerator il = method.GetILGenerator ();
il.DeclareLocal (typeof (object));
il.Emit (OpCodes.Ldtoken, type);
il.Emit (OpCodes.Call, gettype);
il.Emit (OpCodes.Call, newobject);
il.Emit (OpCodes.Stloc_0);
il.Emit (OpCodes.Ldloc_0);
il.Emit (OpCodes.Ldarg_0);
il.Emit (OpCodes.Stfld, handle);
il.Emit (OpCodes.Ldloc_0);
var len = cinfo.GetParameters ().Length;
for (int i = 0; i < len; i++) {
il.Emit (OpCodes.Ldarg, 1);
il.Emit (OpCodes.Ldc_I4, i);
il.Emit (OpCodes.Ldelem_Ref);
}
il.Emit (OpCodes.Call, cinfo);
il.Emit (OpCodes.Ret);
return (Action<IntPtr, object?[]?>) method.CreateDelegate (typeof (Action <IntPtr, object []>));
呼叫返回的委託,使得IntPtr是Java.Lang.Object子類的控制程式碼,而物件[]是該特定c#建構函式的任何引數。emit對於在啟動時第一次使用它以及以後的每次呼叫都有很大的成本。
經過仔細的審查,我們可以將handle欄位設定為內部的,並將此程式碼簡化為:
var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType);
if (newobj is Java.Lang.Object o) {
o.handle = jobject;
} else if (newobj is Java.Lang.Throwable throwable) {
throwable.handle = jobject;
} else {
throw new InvalidOperationException ($"Unsupported type: '{newobj}'");
}
cinfo.Invoke (newobj, parms);
這段程式碼所做的是在不呼叫建構函式的情況下建立一個物件,設定控制程式碼欄位,然後呼叫建構函式。這樣做是為了當c#建構函式開始時,Handle在任何Java.Lang.Object上都是有效的。建構函式內部的任何Java互操作(比如呼叫類上的其他Java方法)以及呼叫任何基本Java建構函式都需要Handle。
新程式碼顯著改進了從Java呼叫的任何c#建構函式,因此這個特殊的更改改進的不僅僅是.NET MAUI。除了.NET 6之外,針對當前客戶Xamarin. android的最新版本也附帶了這一更改。
檢視xamarin-android#6766瞭解這個改進的詳細資訊。
▌System.Reflection.Emit和方法
當你在c#中重寫一個Java方法時,比如:
public class MainActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
//...
}
}
在從Java到c#的轉換過程中,我們必須封裝c#方法來處理異常,例如:
try
{
// Call the actual C# method here
}
catch (Exception e) when (_unhandled_exception (e))
{
androidEnvironment.UnhandledException (e);
if (Debugger.IsAttached || !JNIEnv.PropagateExceptions)
throw;
}
例如,如果在OnCreate()中未處理託管異常,那麼實際上會導致本機崩潰(並且沒有託管的c#堆疊跟蹤)。我們需要確保偵錯程式在附加異常時能夠中斷,否則將記錄c#堆疊跟蹤。
從Xamarin開始,上面的程式碼是通過System.Reflection.Emit生成的:
var dynamic = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), ret_type, param_types, typeof (DynamicMethodNameCounter), true);
var ig = dynamic.GetILGenerator ();
LocalBuilder? retval = null;
if (ret_type != typeof (void))
retval = ig.DeclareLocal (ret_type);
ig.Emit (OpCodes.Call, wait_for_bridge_processing_method!);
var label = ig.BeginExceptionBlock ();
for (int i = 0; i < param_types.Length; i++)
ig.Emit (OpCodes.Ldarg, i);
ig.Emit (OpCodes.Call, dlg.Method);
if (retval != null)
ig.Emit (OpCodes.Stloc, retval);
ig.Emit (OpCodes.Leave, label);
bool filter = Debugger.IsAttached || !JNIEnv.PropagateExceptions;
if (filter && JNIEnv.mono_unhandled_exception_method != null) {
ig.BeginExceptFilterBlock ();
ig.Emit (OpCodes.Call, JNIEnv.mono_unhandled_exception_method);
ig.Emit (OpCodes.Ldc_I4_1);
ig.BeginCatchBlock (null!);
} else {
ig.BeginCatchBlock (typeof (Exception));
}
ig.Emit (OpCodes.Dup);
ig.Emit (OpCodes.Call, exception_handler_method!);
if (filter)
ig.Emit (OpCodes.Throw);
ig.EndExceptionBlock ();
if (retval != null)
ig.Emit (OpCodes.Ldloc, retval);
ig.Emit (OpCodes.Ret);
這段程式碼被呼叫兩次為一個 dotnet new android 應用程式,但~58次為一個dotnet new maui應用程式!
我們意識到實際上可以為每個通用委託型別編寫一個強型別的“快速路徑”,而不是使用System.Reflection.Emit。有一個生成的委託匹配每個簽名:
void OnCreate(Bundle savedInstanceState);
// Maps to *JNIEnv, JavaClass, Bundle
// Internal to each assembly
internal delegate void _JniMarshal_PPL_V(IntPtr, IntPtr, IntPtr);
這樣我們就可以列出所有使用過的dotnet maui應用程式的簽名,比如:
class JNINativeWrapper
{
static Delegate? CreateBuiltInDelegate (Delegate dlg, Type delegateType)
{
switch (delegateType.Name)
{
// Unsafe.As<T>() is used, because _JniMarshal_PPL_V is generated internal in each assembly
case nameof (_JniMarshal_PPL_V):
return new _JniMarshal_PPL_V (Unsafe.As<_JniMarshal_PPL_V> (dlg).Wrap_JniMarshal_PPL_V);
// etc.
}
return null;
}
// Static extension method is generated to avoid capturing variables in anonymous methods
internal static void Wrap_JniMarshal_PPL_V (this _JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
{
// ...
}
}
這種方法的缺點是,當使用新簽名時,我們必須列出更多的情況。我們不想詳盡地列出每一種組合,因為這會導致IL大小的增長。我們正在研究如何在未來的.NET版本中改進這一點。
檢視xamarin-android#6657和xamarin-android#6707瞭解這個改進的詳細資訊。
▌更新的Java.Interop APIs
Java.Interop.dll中原始的Xamarin api是這樣的api:
- JNIEnv.CallStaticObjectMethod
在Java中呼叫的“新方法”每次呼叫佔用的記憶體更少:
- JniEnvironment.StaticMethods.CallStaticObjectMethod
當在構建時為Java方法生成c#繫結時,預設使用更新/更快的方法—在Xamarin.Android中已經有一段時間了。以前,Java繫結專案可以將$(AndroidCodegenTarget)設定為XAJavaInterop1,它在每次呼叫中快取和重用jmethodID例項。請參閱java.interop文件獲取關於該特性的歷史記錄。
其他有問題的地方是有“手動”繫結的地方。這些往往也是經常使用的方法,所以值得修復這些!
一些改善這種情況的例子:
- JNIEnv.FindClass()在xamarin-android#6805
- JavaList 和 JavaList<T>在 xamarin-android#6812
$(AndroidCodegenTarget):
https://docs.microsoft.com/en...
java.interop:
https://github.com/xamarin/Ja...
xamarin-android#6805:
https://github.com/xamarin/xa...
xamarin-android#6812:
https://github.com/xamarin/xa...
▌多維Java陣列
當向Java來回傳遞c#陣列時,中間步驟必須複製陣列,以便適當的執行時能夠訪問它。這真的是一個開發者體驗的情況,因為c#開發者期望寫這樣的東西:
var array = new int[] { 1, 2, 3, 4};
MyJavaMethod (array);
在MyJavaMethod裡面會做:
IntPtr native_items = JNIEnv.NewArray (items);
try
{
// p/invoke here, actually calls into Java
}
finally
{
if (items != null)
{
JNIEnv.CopyArray (native_items, items); // If the calling method mutates the array
JNIEnv.DeleteLocalRef (native_items); // Delete our Java local reference
}
}
JNIEnv.NewArray()訪問一個“型別對映”,以知道需要將哪個Java類用於陣列的元素。
dotnet new maui專案使用的特定android API有問題:
public ColorStateList (int[][]? states, int[]? colors)
發現一個多維 int[][] 陣列可以訪問每個元素的“型別對映”。當啟用額外的日誌記錄時,我們可以看到這一點,許多例項:
monodroid: typemap: failed to map managed type to Java type: System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e (Module ID: 8e4cd939-3275-41c4-968d-d5a4376b35f5; Type token: 33554653)
monodroid-assembly: typemap: called from
monodroid-assembly: at android.Runtime.JNIEnv.TypemapManagedToJava(Type )
monodroid-assembly: at android.Runtime.JNIEnv.GetJniName(Type )
monodroid-assembly: at android.Runtime.JNIEnv.FindClass(Type )
monodroid-assembly: at android.Runtime.JNIEnv.NewArray(Array , Type )
monodroid-assembly: at android.Runtime.JNIEnv.NewArray[Int32[]](Int32[][] )
monodroid-assembly: at android.Content.Res.ColorStateList..ctor(Int32[][] , Int32[] )
monodroid-assembly: at Microsoft.Maui.Platform.ColorStateListExtensions.CreateButton(Int32 enabled, Int32 disabled, Int32 off, Int32 pressed)
對於這種情況,我們應該能夠呼叫JNIEnv.FindClass()一次,併為陣列中的每一項重用這個值!
我們正在研究如何在未來的.NET版本中進一步改進這一點。一個這樣的例子是dotnet/maui#5654,在這裡我們只是簡單地考慮完全用Java來建立陣列。
檢視xamarin-android#6870瞭解這個改進的詳細資訊。
▌為android影像使用Glide
Glide是現代android應用程式推薦的圖片載入庫。谷歌文件甚至推薦使用它,因為內建的android Bitmap類可能很難正確使用。glidex.forms是在Xamarin.Forms中使用Glide的原型。但我們將 Glide 提升為未來在 .NET MAUI 中載入影像的“方式”。
為了減少JNI互操作的開銷,.NET MAUI的Glide實現主要是用Java編寫的,例如:
import com.bumptech.glide.Glide;
//...
public static void loadImageFromUri(ImageView imageView, String uri, Boolean cachingEnabled, ImageLoaderCallback callback) {
//...
RequestBuilder<Drawable> builder = Glide
.with(imageView)
.load(androidUri);
loadInto(builder, imageView, cachingEnabled, callback);
}
ImageLoaderCallback在c#中子類化以處理託管程式碼中的完成。其結果是,來自web的影像的效能應該比以前在Xamarin.Forms中得到的效能有了顯著提高。
詳見dotnet/maui#759和dotnet/maui#5198。
▌減少Java互操作呼叫
假設你有以下Java api:
public void setFoo(int foo);
public void setBar(int bar);
這些方法的互操作如下:
public unsafe static void SetFoo(int foo)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
__args[0] = new JniArgumentValue(foo);
return _members.StaticMethods.InvokeInt32Method("setFoo.(I)V", __args);
}
public unsafe static void SetBar(int bar)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
__args[0] = new JniArgumentValue(bar);
return _members.StaticMethods.InvokeInt32Method("setBar.(I)V", __args);
}
所以呼叫這兩個方法會兩次呼叫stackalloc,兩次呼叫p/invoke。建立一個小型的Java包裝器會更有效能,例如:
public void setFooAndBar(int foo, int bar)
{
setFoo(foo);
setBar(bar);
}
翻譯為:
public unsafe static void SetFooAndBar(int foo, int bar)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[2];
__args[0] = new JniArgumentValue(foo);
__args[1] = new JniArgumentValue(bar);
return _members.StaticMethods.InvokeInt32Method("setFooAndBar.(II)V", __args);
}
.NET MAUI檢視本質上是c#物件,有很多屬性需要在Java中以完全相同的方式設定。如果我們將這個概念應用到.NET MAUI中的每個android View中,我們可以建立一個~18引數的方法用於View建立。後續的屬性更改可以直接呼叫標準的android api。
對於非常簡單的.NET MAUI控制元件來說,這在效能上有了顯著的提高:
請參閱dotnet/maui#3372瞭解有關此改進的詳細資訊。
▌將android XML移植到Java
回顧android上的dotnet跟蹤輸出,我們可以看到合理的時間花費在:
20.32.ms mono.andorid!Andorid.Views.LayoutInflater.Inflate
回顧堆疊跟蹤,時間實際上花在了android/Java擴充套件布局上,而在.NET端沒有任何工作發生。
如果你看看編譯過的android .apk和res/layouts/bottomtablayout。在android Studio中,XML只是普通的XML。只有少數識別符號被轉換為整數。這意味著android必須解析XML並通過Java的反射api建立Java物件——似乎我們不使用XML就可以獲得更快的效能?
通過標準的BenchmarkDotNet對比,我們發現在涉及互操作時,使用android佈局的表現甚至比使用c#更差:
接下來,我們將BenchmarkDotNet配置為單次執行,以更好地模擬啟動時發生的情況:
方法
我們在.NET MAUI中看到了一個更簡單的佈局,底部標籤導航:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/bottomtab.navarea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="fill"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomtab.tabbar"
android:theme="@style/Widget.Design.BottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
我們可以將其移植到四個Java方法中,例如:
@NonNull
public static List<View> createBottomTabLayout(Context context, int navigationStyle);
@NonNull
public static LinearLayout createLinearLayout(Context context);
@NonNull
public static FrameLayout createFrameLayout(Context context, LinearLayout layout);
@NonNull
public static BottomNavigationView createNavigationBar(Context context, int navigationStyle, FrameLayout bottom)
這使得我們在android上建立底部標籤導航時只能從c#切換到Java 4次。它還允許android作業系統跳過載入和解析.xml來“膨脹”Java物件。我們在dotnet/maui中執行了這個想法,在啟動時刪除所有LayoutInflater.Inflate()呼叫。
請參閱dotnet/maui#5424, dotnet/maui#5493,和dotnet/maui#5528瞭解這些改進的詳細資訊。
▌刪除Microsoft.Extensions.Hosting
hosting提供了一個.NET通用主機,用於在.NET應用程式中管理依賴注入、日誌記錄、配置和應用生命週期。這對啟動時間有影響,似乎不適合移動應用程式。
從.NET MAUI中移除Microsoft.Extensions.Hosting使用是有意義的。. net MAUI沒有試圖與“通用主機”互操作來構建DI容器,而是有自己的簡單實現,它針對移動啟動進行了優化。此外,. net MAUI預設不再新增日誌記錄提供程式。
通過這一改變,我們看到dotnet new maui android應用程式的啟動時間減少了5-10%。在iOS上,它減少了相同應用程式的大小,從19.2 MB => 18.0 MB。
詳見dotnet/maui#4505和dotnet/maui#4545。
▌在啟動時減少Shell初始化
Xamarin. Forms Shell是跨平臺應用程式導航的一種模式。這個模式是在.NET MAUI中提出的,它被推薦作為構建應用程式的預設方式。
當我們發現在啟動時使用Shell的成本(對於Xamarin和Xamarin.form和.NET MAUI),我們找到了幾個可以優化的地方:
- 不要在啟動時解析路由——要等到一個需要它們的導航發生。
- 如果沒有為導航提供查詢字串,則只需跳過處理查詢字串的程式碼。這將刪除過度使用System.Reflection的程式碼路徑。
- 如果頁面沒有可見的BottomNavigationView,那麼不要設定選單項或任何外觀元素。
請參閱dotnet/maui#5262瞭解此改進的詳細資訊。
▌字型不應該使用臨時檔案
大量的時間花在.NET MAUI應用程式載入字型上:
32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3<string, Microsoft.Maui.FontWeight, bool>)
檢查程式碼時,它所做的工作比需要的更多:
1.將androidAsset檔案儲存到臨時資料夾。
2.使用android API, Typeface.CreateFromFile()來載入檔案。
我們實際上可以直接使用Typeface.CreateFromAsset() android API,根本不用臨時檔案。
請參閱dotnet/maui#4933瞭解有關此改進的詳細資訊。
▌編譯時在平臺上計算
{OnPlatform}標記擴充套件的使用:
<Label Text="Platform: " />
<Label Text="{OnPlatform Default=Unknown, android=android, iOS=iOS" />
…實際上可以在編譯時計算,net6.0-android和net6.0-ios會得到適當的值。在未來的.NET版本中,我們將對 XML元素進行同樣的優化。
詳見dotnet/maui#4829和dotnet/maui#5611。
▌在XAML中使用編譯轉換器
以下型別現在在XAML編譯時轉換,而不是在執行時:
- 顏色:dotnet /maui# 4687
- 角半徑: dotnet / maui # 5192
- 字形大小:dotnet / maui # 5338
- 網格長度, 行定義, 列定義: dotnet/maui#5489
這導致從.xaml檔案生成更好/更快的IL。
▌優化顏色解析
Microsoft.Maui.Graphics.Color.Parse()的原始程式碼可以重寫,以更好地使用Span並避免字串分配。
能夠在ReadonlySpan<char> dotnet/csharplang#1881上使用switch語句,將在未來的.NET版本中進一步改善這種情況。
看到dotnet / Microsoft.Maui.Graphics # 343和dotnet / Microsoft.Maui.Graphics # 345關於這個改進的細節。
▌不要使用區域性識別的字串比較
回顧一個新的naui專案的dotnet跟蹤輸出,可以看到android上第一個區域性感知字串比較的真實成本:
6.32ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellNavigationManager.GetNavigationState
3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri
3.82ms System.Private.CoreLib!System.String.StartsWith
2.57ms System.Private.CoreLib!System.Globalization.CultureInfo.get_CurrentCulture
實際上,我們甚至不希望在本例中使用區域性比較—它只是從Xamarin.Forms引入的程式碼。
例如,如果你有:
if (text.StartsWith("f"))
{
// do something
}
在這種情況下,你可以簡單地這樣做:
if (text.StartsWith("f", StringComparision.Ordinal))
{
// do something
}
如果在整個應用程式中執行,System.Globalization.CultureInfo.CurrentCulture可以避免被呼叫,並且可以稍微提高If語句的總體速度。
為了解決整個dotnet/maui回購的這種情況,我們引入了程式碼分析規則來捕捉這些:
dotnet_diagnostic.CA1307.severity = error
dotnet_diagnostic.CA1309.severity = error
請參閱dotnet/maui#4988瞭解有關改進的詳細資訊。
▌懶惰地建立日誌
ConfigureFonts() API在啟動時花費了一些時間來做一些可以延遲到以後的工作。我們還可以改進Microsoft.Extensions中日誌基礎設施的一般用法。
我們所做的一些改進如下:
- 推遲建立“記錄器”類,直到需要它們時再建立。
- 內建的日誌記錄基礎設施在預設情況下是禁用的,必須顯式啟用。
- 延遲呼叫android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。
- 不要使用ILoggerFactory建立通用記錄器。而是直接獲取ILogger服務,這樣它就被快取了。
請參閱dotnet/maui#5103瞭解有關此改進的詳細資訊。
▌使用工廠方法進行依賴注入
當使用Microsoft.Extensions。DependencyInjection,註冊服務,比如:
IServiceCollection services /* ... */;
services.TryAddSingleton<IFooService, FooService>();
Microsoft.Extensions必須做一些System.Reflection來建立FooService的第一個例項。這是值得注意的dotnet跟蹤輸出在android上。
相反,如果你這樣做了:
// If FooService has no dependencies
services.TryAddSingleton<IFooService>(sp => new FooService());
// Or if you need to retrieve some dependencies
services.TryAddSingleton<IFooService>(sp => new FooService(sp.GetService<IBar>()));
在這種情況下,Microsoft.Extensions可以簡單地呼叫lamdba/匿名方法,而不需要系統。反射。
我們在所有的dotnet/maui上進行了改進,並使用了bannedapianalyzer,這樣就不會有人意外地使用TryAddSingleton()更慢的過載。
請參閱dotnet/maui#5290瞭解有關此改進的詳細資訊。
▌懶惰地負載ConfigurationManager
configurationmanager並沒有被許多移動應用程式使用,而且建立一個是非常昂貴的!(例如,在android上約為7.59ms)
在.NET MAUI中,一個ConfigurationManager在啟動時預設被建立,我們可以使用Lazy延遲它的建立,所以它將不會被建立,除非請求。
請參閱dotnet/maui#5348瞭解有關此改進的詳細資訊。
▌預設VerifyDependencyInjectionOpenGenericServiceTrimmability
.NET Podcast樣本花費了4-7ms的時間:
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()
MSBuild屬性$(verifydependencyinjectionopengenericservicetrimability)觸發該方法執行。這個特性開關確保dynamallyaccessedmembers被正確地應用於開啟依賴注入中的泛型型別。
在基礎.NET SDK中,當publishtrim =true時,該開關將被啟用。然而,android應用程式在Debug版本中並沒有設定publishtrim =true,所以開發者錯過了這個驗證。
相反,在已釋出的應用程式中,我們不想支付這種驗證的成本。所以這個特性開關應該在Release版本中關閉。
檢視xamarin-android#6727和xamarin-macios#14130瞭解關於這個改進的詳細資訊。
▌改進內建AOT配置檔案
Mono執行時有一個關於每個方法的JIT時間的報告(參見我們的文件),例如:
Total(ms) | Self(ms) | Method
3.51 | 3.51 | Microsoft.Maui.Layouts.GridLayoutManager/GridStructure:.ctor (Microsoft.Maui.IGridLayout,double,double)
1.88 | 1.88 | Microsoft.Maui.Controls.Xaml.AppThemeBindingExtension/<>c__DisplayClass20_0:<Microsoft.Maui.Controls.Xaml.IMarkupExtension<Microsoft.Maui.Controls.BindingBase>.ProvideValue>g__minforetriever|0 ()
1.66 | 1.66 | Microsoft.Maui.Controls.Xaml.OnIdiomExtension/<>c__DisplayClass32_0:<ProvideValue>g__minforetriever|0 ()
1.54 | 1.54 | Microsoft.Maui.Converters.ThicknessTypeConverter:ConvertFrom (System.ComponentModel.ITypeDescriptorContext,System.Globalization.CultureInfo,object)
這是一個使用Profiled AOT的版本構建中.NET Podcast示例中的頂級jit時間選擇。這些似乎是開發人員希望在. net MAUI應用程式中使用的常用api。
為了確保這些方法在AOT配置檔案中,我們在dotnet/maui中使用了這些api
_=new Microsoft.Maui.Layouts.GridLayoutManager(new Grid()).Measure(100, 100);
<SolidColorBrush x:Key="ProfiledAot_AppThemeBinding_Color" Color="{AppThemeBinding Default=Black}"/>
<CollectionView x:Key="ProfiledAot_CollectionView_OnIdiom_Thickness" Margin="{OnIdiom Default=1,1,1,1}" />
在這個測試應用程式中呼叫這些方法可以確保它們位於內建的. net MAUI AOT配置檔案中。
在這個更改之後,我們看了一個更新的JIT報告:
Total (ms) | Self (ms) | Method
2.61 | 2.61 | string:SplitInternal (string,string[],int,System.StringSplitOptions)
1.57 | 1.57 | System.Number:NumberToString (System.Text.ValueStringBuilder&,System.Number/NumberBuffer&,char,int,System.Globalization.NumberFormatInfo)
1.52 | 1.52 | System.Number:TryParseInt32IntegerStyle (System.ReadOnlySpan`1<char>,System.Globalization.NumberStyles,System.Globalization.NumberFormatInfo,int&)
這導致了進一步的補充:
var split = "foo;bar".Split(';');
var x = int.Parse("999");
x.ToString();
我們對Color.Parse()、Connectivity做了類似的修改.NETworkAccess DeviceInfo。成語,AppInfo。.NET MAUI應用程式中應該經常使用的requestdtheme。
請參閱dotnet/maui#5559, dotnet/maui#5682,和dotnet/maui#6834瞭解這些改進的詳細資訊。
如果你想在.NET 6中記錄一個自定義的AOT配置檔案,你可以嘗試我們的實驗包Mono.Profiler.Android。我們正在努力在未來的.NET版本中完全支援記錄自定義概要檔案。
▌啟用AOT影像的延遲載入
以前,Mono執行時將在啟動時載入所有AOT影像,以驗證託管.NET程式集(例如Foo.dll)的MVID是否與AOT影像(libFoo.dll.so)匹配。在大多數.NET應用程式中,一些AOT映像可能稍後才需要載入。
Mono中引入了一個新的——aot-lazy-assembly-load或mono_opt_aot_lazy_assembly_load設定,android工作負載可以選擇。我們發現這將dotnet new maui專案在Pixel 6 Pro上的啟動時間提高了約25ms。
這是預設啟用的,但如果需要,你可以在你的。csproj中通過以下方式禁用此設定:
<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>
檢視dotnet/runtime#67024和xamarin-android #6940瞭解這些改進的詳細資訊。
▌刪除System.Uri中未使用的編碼物件
一個MAUI應用程式的dotnet跟蹤輸出,顯示大約7ms花費了載入UTF32和Latin1編碼的第一次系統。使用Uri api:
namespace System
{
internal static class UriHelper
{
internal static readonly Encoding s_noFallbackCharUTF8 = Encoding.GetEncoding(
Encoding.UTF8.CodePage, new EncoderReplacementFallback(""), new DecoderReplacementFallback(""));
這個欄位是不小心留在原地的。只需刪除s_noFallbackCharUTF8欄位,就可以改進任何使用System.Uri 或相關的api的. net應用程式的啟動。
參見dotnet/runtime#65326瞭解有關此改進的詳細資訊。
應用程式大小的改進
▌修復預設的MauiImage大小
dotnet new maui模板顯示一個友好的"網路機器人”的形象。這是通過使用一個.svg檔案作為一個MauiImage和內容來實現的:
<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- everything else -->
預設情況下,MauiImage使用.svg中的寬度和高度值作為影像的“基礎大小”。回顧構建輸出,這些影像被縮放為:
objReleasenet6.0-androidresizetizerrmipmap-xxxhdpi
appiconfg.png = 1824x1824
dotnet_bot.png = 1676x2076
這對於android裝置來說似乎有點太大了?我們可以簡單地在模板中指定%(BaseSize),它還提供了一個如何為這些影像選擇合適大小的示例:
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\appiconfg.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
這就產生了更合適的尺寸:
obj\Release\net6.0-android\resizetizer\r\mipmap-xxxhdpi\
appiconfg.png = 512x512
dotnet_bot.png = 672x832
我們還可以修改.svg內容,但這可能不可取,這取決於圖形設計師如何在其他設計工具中使用該影像。
在另一個例子中,一個3008×5340 .jpg影像:
<MauiImage Include="Resources\Images\large.jpg" />
正在升級到21360×12032!設定Resize="false"將防止影像被調整大小,但我們將此設定為非向量影像的預設選項。接下來,開發人員應該能夠依賴預設值,或者根據需要指定%(基本尺寸)和%(調整大小)。
這些改變改善了啟動效能和應用程式的大小。請參閱dotnet/maui#4759和dotnet/maui#6419瞭解這些改進的細節。
▌刪除Application.Properties 和DataContractSerializer
Xamarin.Forms 有一個 API,用於通過 Application.Properties 字典持久化鍵值對。這在內部使用了DataContractSerializer,這對於自包含和修剪的移動應用程式不是最佳選擇。來自BCL的System.Xml的部分可能相當大,我們不想在每個.NET MAUI應用程式中都為此付出代價。
簡單地刪除這個API和所有DataContractSerializer的使用,在android上可以提高約855KB,在iOS上提高約1MB。
請參閱dotnet/maui#4976瞭解有關此改進的詳細資訊。
▌修剪未使用的HTTP實現
System.NET.Http.UseNativeHttpHandler沒有適當地削減底層託管HTTP處理程式(SocketsHttpHandler)。預設情況下,androidMessageHandler和NSUrlSessionHandler被用來利用底層的android和iOS網路棧。
通過修正這個問題,在任何.NET MAUI應用程式中都可以刪除更多的IL程式碼。在一個例子中,一個使用HTTP的android應用程式能夠完全刪除幾個程式集:
- Microsoft.Win32.Primitives.dll
- System.Formats.Asn1.dll
- System.IO.Compression.Brotli.dll
- System.NET.NameResolution.dll
- System.NET.NETworkInformation.dll
- System.NET.Quic.dll
- System.NET.Security.dll
- System.NET.Sockets.dll
- System.Runtime.InteropServices.RuntimeInformation.dll
- System.Runtime.Numerics.dll
- System.Security.Cryptography.Encoding.dll
- System.Security.Cryptography.X509Certificates.dll
- System.Threading.Channels.dll
檢視dotnet/runtime#64852, xamarin-android#6749,和xamarin-macios#14297關於這個改進的詳細資訊。
.NET Podcast示例中的改進
我們對樣本本身做了一些調整,其中更改被認為是“最佳實踐”。
▌刪除Microsoft.Extensions.Http用法
使用Microsoft.Extensions.Http對於移動應用程式來說太重了,並且在這種情況下沒有提供任何真正的價值。
因此,HttpClient不使用DI:
builder.Services.AddHttpClient<ShowsService>(client =>
{
client.BaseAddress = new Uri(Config.APIUrl);
});
// Then in the service ctor
public ShowsService(HttpClient httpClient, ListenLaterService listenLaterService)
{
this.httpClient = httpClient;
// ...
}
我們簡單地建立一個HttpClient來在服務中使用:
public ShowsService(ListenLaterService listenLaterService)
{
this.httpClient = new HttpClient() { BaseAddress = new Uri(Config.APIUrl) };
// ...
}
我們建議對應用程式需要互動的每個web服務使用一個單獨的HttpClient例項。
請參閱dotnet/runtime#66863和dotnet podcasts#44瞭解有關改進的詳細資訊。
▌刪除Newtonsoft.Json使用
.NET Podcast 樣本使用了一個名為MonkeyCache的庫,它依賴於Newtonsoft.Json。這本身並不是一個問題,只是.NET MAUI + Blazor應用程式依賴於一些ASP.NET Core庫反過來依賴於System.Text.Json。這款應用實際上是為JSON解析庫“付了兩倍錢”,這對應用的大小產生了影響。
我們移植了MonkeyCache 2.0來使用System.Text。Json,不需要Newtonsoft。這將iOS上的應用大小從29.3MB減少到26.1MB!
參見monkey-cache#109和dotnet-podcasts#58瞭解有關改進的詳細資訊。
▌在後臺執行第一個網路請求
回顧dotnet跟蹤輸出,初始請求在ShowsService阻塞UI執行緒初始化連線.NETworkAccess Barrel.Current。得到,HttpClient。這項工作可以在後臺執行緒中完成-在這種情況下導致更快的啟動時間。在Task.Run()中封裝第一個呼叫,可以在一定程度上提高這個示例的啟動效率。
在Pixel 5a裝置上平均執行10次:
Before
Average(ms): 843.7
Average(ms): 847.8
After
Average(ms): 817.2
Average(ms): 812.8
對於這種型別的更改,總是建議根據dotnet跟蹤或其他分析結果來做出決定,並度量更改前後的變化。
請參閱dotnet-podcasts#57瞭解有關此改進的詳細資訊。
實驗性或高階選項
如果你想在android上進一步優化你的.NET MAUI應用程式,這裡有一些高階或實驗性的特性,預設情況下不是啟用的。
▌修剪Resource.designer.cs
自從Xamarin誕生以來,android應用程式就包含了一個生成的Properties/Resource.designer.cs檔案,用於訪問androidResource檔案的整數識別符號。這是R.java類的c# /託管版本,允許使用這些識別符號作為普通的c#欄位(有時是const),而無需與Java進行任何互操作。
在一個android Studio“庫”專案中,當你包含一個像res/drawable/foo.png這樣的檔案時,你會得到一個像這樣的欄位:
package com.yourlibrary;
public class R
{
public class drawable
{
// The actual integer here maps to a table inside the final .apk file
public final int foo = 1234;
}
}
你可以使用這個值,例如,在ImageView中顯示這個影像:
ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.foo);
當你構建com.yourlibrary.aar時, android的gradle外掛實際上並沒有把這個類放在包中。相反,android應用程式實際上知道整數的值是多少。因此,R類是在android應用程式構建時生成的,為每個android庫生成一個R類。
Xamarin.Android採取了不同的方法,在執行時進行整數修復。用c#和MSBuild做這樣的事情真的沒有一個很好的先例嗎?例如,一個c# android庫可能有:
public class Resource
{
public class Drawable
{
// The actual integer here is *not* final
public int foo = -1;
}
}
然後主應用程式就會有如下程式碼:
public class Resource
{
public class Drawable
{
public Drawable()
{
// Copy the value at runtime
global::MyLibrary.Resource.Drawable.foo = foo;
}
// The actual integer here *is* final
public const int foo = 1234;
}
}
這種情況已經很好地執行了一段時間,但不幸的是,像androidX、Material、谷歌Play Services等谷歌的庫中的資源數量已經開始複合。例如,在dotnet/maui#2606中,啟動時設定了21497個欄位!我們建立了一種方法來解決這個問題,但我們也有一個新的自定義修剪步驟來執行修復在構建時(在修剪期間)而不是在執行時。
<AndroidLinkResources>true</ AndroidLinkResources>
這將使你的版本版本替換案例如下:
ImageView imageView = new(this);
imageView.SetImageResource(Resource.Drawable.foo);
相反,直接內聯整數:
ImageView imageView = new(this);
imageView.SetImageResource(1234); // The actual integer here *is* final
這個特性的一個已知問題是:
public partial class Styleable
{
public static int[] ActionBarLayout = new int[] { 16842931 };
}
目前不支援替換int[]值,這使得我們不能預設啟用它。一些應用程式將能夠開啟這個功能,dotnet新的maui模板,也許許多.NET maui android應用程式不會遇到這個限制。
在未來的.NET版本中,我們可能會預設啟用$(androidLinkResources),或者完全重新設計。
檢視xamarin-android#5317, xamarin-android#6696,和dotnet/maui#4912瞭解該功能的詳細資訊。
▌R8 Java程式碼收縮器
R8是全程式優化、收縮和縮小工具,將java位元組程式碼轉換為優化的dex程式碼。R8使用Proguard keep規則格式為應用程式指定入口點。如您所料,許多應用程式需要額外的Proguard規則來保持工作。R8可能過於激進,並且刪除了Java反射所呼叫的一些東西,等等。我們還沒有一個很好的方法讓它成為所有.NET android應用程式的預設設定。
要選擇使用R8 for Release版本,請在你的.csproj中新增以下內容:
<!-- NOTE: not recommended for Debug builds! -->
<AndroidLinkTool Condition="'$(Configuration)' == 'Release'">r8</AndroidLinkTool>
如果啟動你的應用程式的Release構建在啟用後崩潰,檢查adb logcat輸出,看看哪裡出了問題。
如果你看到java.lang. classnotfoundexception或java.lang。你可能需要新增一個ProguardConfiguration檔案到你的專案中,比如:
<ItemGroup>
<ProguardConfiguration Include="proguard.cfg" />
</ItemGroup>
-keep class com.thepackage.TheClassYouWantToPreserve { *; <init>(...); }
我們正在研究在未來的.NET版本中預設啟用R8的選項。
詳情請參閱我們的D8/R8文件。
▌AOT
Profiled AOT是預設的,因為它在應用程式大小和啟動效能之間給出了最好的權衡。如果應用程式的大小與你的應用程式無關,你可以考慮對所有.NET程式集使用AOT。
要選擇加入,在你的.csproj中新增以下Release配置:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<androidEnableProfiledAot>false</androidEnableProfiledAot>
</PropertyGroup>
這將減少在應用程式啟動期間發生的JIT編譯量,以及導航到後面的螢幕等。
▌AOT和LLVM
LLVM提供了一個獨立於源和目標的現代優化器,可以與Mono AOT Compiler輸出相結合。其結果是,應用的尺寸略大,發行構建時間更長,執行時效能更好。
要選擇將LLVM用於Release版本,請將以下內容新增到你的.csproj中:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<EnableLLVM>true</EnableLLVM>
</PropertyGroup>
此特性可以與Profiled AOT(或AOT-ing一切)結合使用。對比應用程式的前後,瞭解EnableLLVM對應用程式大小和啟動效能的影響。
目前,需要安裝一個android NDK來使用這個功能。如果我們能夠解決這個需求,EnableLLVM將成為未來.NET版本中的預設選項。
有關詳細資訊,請參閱我們關於EnableLLVM的文件。
LLVM:
EnableLLVM的文件:
▌記錄自定義AOT配置檔案
概要AOT預設使用我們在.NET MAUI和android工作負載中提供的“內建”概要檔案,對大多數應用程式都很有用。為了獲得最佳的啟動效能,理想情況下應該記錄應用程式特定的配置檔案。針對這種情況,我們有一個實驗性的Mono.Profiler.Android包。
記錄配置檔案:
dotnet add package Mono.AotProfiler.android
dotnet build -t:BuildAndStartAotProfiling
# Wait until app launches, or you navigate to a screen
dotnet build -t:FinishAotProfiling
這將在你的專案目錄下產生一個custom.aprof。要在未來的構建中使用它:
<ItemGroup>
<androidAotProfile Include="custom.aprof" />
</ItemGroup>
我們正在努力在未來的.NET版本中完全支援記錄自定義概要檔案。
結論
我希望您喜歡我們的.NET MAUI效能論述。請嘗試.NET MAUI並且可以在http://dot.net/maui瞭解更多!
長按識別二維碼
關注微軟中國MSDN