.NET MAUI 效能提升

微軟技術棧 發表於 2022-07-01
.Net

.NET多平臺應用程式UI (MAUI)將android、iOS、macOS和Windows API統一為一個API,這樣你就可以編寫一個應用程式在許多平臺上本機執行。我們專注於提高您的日常生產力以及您的應用程式的效能。我們認為,開發人員生產率的提高不應該以應用程式效能為代價。

應用程式的大小也是如此——在一個空白的.NET MAUI應用程式中存在什麼開銷?當我們開始優化.NET MAUI時,很明顯iOS需要做一些工作來改善應用程式的大小,而android則缺乏啟動效能。

一個dotnet new maui專案的iOS應用程式最初大約是18MB。同樣,在之前的預覽中.NET MAUI在android上的啟動時間也不是很理想:
image.png
.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程式碼,等等方面。
image.png


.NET Podcast App (Shell):https://github.com/microsoft/...
**這是原始的dotnet new maui模板,沒有使用Shell。

內容十分豐富,來看是否有您期待的更新吧!

主要內容

啟動效能的改進

應用程式大小的改進

.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上開啟這個檔案,深入瞭解每個方法在應用程式啟動期間所花費的時間:
.NET MAUI 效能提升
圖片

在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# 520dotnet/maui#6419瞭解這些改進的詳細資訊。

▌異形AOT

在我們對.NET MAUI的初始效能測試中,我們看到了JIT(及時)和AOT(提前)編譯的程式碼是如何執行的:
image.png

每次呼叫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#6547dotnet/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#6657xamarin-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#759dotnet/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控制元件來說,這在效能上有了顯著的提高:
image.png

請參閱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#更差:
image.png

接下來,我們將BenchmarkDotNet配置為單次執行,以更好地模擬啟動時發生的情況:
方法
image.png
我們在.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#4505dotnet/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#4829dotnet/maui#5611

▌在XAML中使用編譯轉換器

以下型別現在在XAML編譯時轉換,而不是在執行時:

▌優化顏色解析

Microsoft.Maui.Graphics.Color.Parse()的原始程式碼可以重寫,以更好地使用Span並避免字串分配。

image.png
能夠在ReadonlySpan<char> dotnet/csharplang#1881上使用switch語句,將在未來的.NET版本中進一步改善這種情況。

看到dotnet / Microsoft.Maui.Graphics # 343dotnet / 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#6727xamarin-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#67024xamarin-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#4759dotnet/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#66863dotnet 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#109dotnet-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瞭解更多!


.NET MAUI 效能提升
長按識別二維碼
關注微軟中國MSDN

點選獲取學習資源~