點擊藍字
關注我們
作者:Jonathan Peppers
翻譯:Yijing Sun
校稿:Amy Peng
排版:Rani Sun
精彩預告
*本文干貨滿滿,預計閱讀時間32分鐘,建議收藏保存。
.NET多平臺應用程序UI (MAUI)將android、iOS、macOS和Windows API統一為一個API,這樣你就可以編寫一個應用程序在許多平臺上本機運行。我們專注于提高您的日常生產力以及您的應用程序的性能。我們認為,開發人員生產率的提高不應該以應用程序性能為代價。
應用程序的大小也是如此——在一個空白的.NET MAUI應用程序中存在什么開銷?當我們開始優化.NET MAUI時,很明顯iOS需要做一些工作來改善應用程序的大小,而android則缺乏啟動性能。
一個dotnet new maui項目的iOS應用程序最初大約是18MB。同樣,在之前的預覽中.NET MAUI在android上的啟動時間也不是很理想:
應用程序 | 框架 | 啟動時間(ms) |
Xamarin.Android | Xamarin | 306.5 |
Xamarin.Forms | Xamarin | 498.6 |
Xamarin.Forms (Shell) | Xamarin | 817.7 |
dotnet new android | .NET 6 (早期預覽) | 210.5 |
dotnet new maui | .NET 6 (早期預覽) | 683.9 |
.NET Podcast | .NET 6 (早期預覽) | 1299.9 |
這是在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代碼,等等方面。
塵埃落定后,我們達到了一個更好的階段:
應用程序 | 框架 | 啟動時間(ms) |
Xamarin.Android | Xamarin | 306.5 |
Xamarin.Forms | Xamarin | 498.6 |
Xamarin.Forms (Shell) | Xamarin | 817.7 |
dotnet new android | .NET 6 (MAUI GA) | 182.8 |
dotnet new maui (No Shell**) | .NET 6 (MAUI GA) | 464.2 |
dotnet new maui (Shell) | .NET 6 (MAUI GA) | 568.1 |
.NET Podcast App (Shell) | .NET 6 (MAUI GA) | 814.2 |
** -這是原始的dotnet new maui模板,沒有使用Shell。
下面的細節,享受吧!
.NET Podcast
https://github.com/microsoft/dotnet-podcasts
maui-profiling
https://github.com/jonathanpeppers/maui-profiling
Shell
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/
.NET Podcast App (Shell)
https://github.com/microsoft/dotnet-podcasts
主要內容
啟動性能的改進
在移動設備上進行分析
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiling-on-mobile
測量隨著時間的推移
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#measuring-over-time
Profiled AOT
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiled-aot
單文件程序集存儲器
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores
Spanify.RegisterNativeMembers
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#spanify-registernativemembers
System.Reflection.Emit和構造函數
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-constructors
System.Reflection.Emit和方法
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-methods
更新的Java.Interop APIs
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays
多維Java數組
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays
為android圖像使用Glide
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays
減少Java互操作調用
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#reduce-java-interop-calls
將android XML移植到Java
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#port-android-xml-to-java
刪除Microsoft.Extensions.Hosting
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshosting
在啟動時減少Shell初始化
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#less-shell-initialization-on-startup
字體不應該使用臨時文件
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fonts-should-not-use-temporary-files
編譯時在平臺上計算
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#compute-onplatform-at-compile-time
在XAML中使用編譯轉換器
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-compiled-converters-in-xaml
優化顏色解析
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#optimize-color-parsing
不要使用區域性識別的字符串比較
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#dont-use-culture-aware-string-comparisons
懶惰地創建日志
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily
使用工廠方法進行依賴注入
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-factory-methods-for-dependency-injection
懶惰地負載ConfigurationManager
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily
默認VerifyDependencyInjectionOpenGenericServiceTrimmability
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#default-verifydependencyinjectionopengenericservicetrimmability
改進內置AOT配置文件
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#improve-the-built-in-aot-profile
啟用AOT圖像的延遲加載
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#enable-lazy-loading-of-aot-images
刪除System.Uri中未使用的編碼對象
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-unused-encoding-object-in-systemuri
應用程序大小的改進
修復默認的MauiImage大小
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fix-defaults-for-mauiimage-sizes
刪除Application.Properties 和DataContractSerializer
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-applicationproperties-and-datacontractserializer
修剪未使用的HTTP實現
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trim-unused-http-implementations
.NET Podcast?示例中的改進(https://github.com/microsoft/dotnet-podcasts)
刪除Microsoft.Extensions.Http用法
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshttp-usage
刪除Newtonsoft.Json使用
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-newtonsoftjson-usage
在后臺運行第一個網絡請求
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#run-first-network-request-in-background
實驗性或高級選項
修剪Resource.designer.cs
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trimming-resourcedesignercs
R8 Java代碼收縮器
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#r8-java-code-shrinker
AOT一切
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-everything
AOT和LLVM
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-and-llvm
記錄自定義AOT配置文件
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#record-a-custom-aot-profile
啟動性能的改進
在移動設備上進行分析
我必須提到移動平臺上可用的.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版本,以獲得應用在現實世界中的最佳表現。
dotnet-dsrouter
https://docs.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dsrouter
我們的文檔
https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/tracing.md
測量隨著時間的推移
我們在.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了解這些改進的詳細信息。
dotnet-podcasts#58
https://github.com/microsoft/dotnet-podcasts
Android x# 520
https://github.com/xamarin/AndroidX/pull/520
dotnet/maui#6419
https://github.com/dotnet/maui/pull/6419
異形AOT
在我們對.NET MAUI的初始性能測試中,我們看到了JIT(及時)和AOT(提前)編譯的代碼是如何執行的:
應用 | JIT 時間(ms) | AOT 時間(ms) |
dotnet 新maui | 1078.0ms | 683.9ms |
每次調用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了解這個改進的細節。
啟動跟蹤或分析AOT
https://devblogs.microsoft.com/xamarin/faster-startup-times-with-startup-tracing-on-android/
Mono.Profiler. Android
https://github.com/jonathanpeppers/Mono.Profiler.Android
xamarin-Android#6547
https://github.com/xamarin/xamarin-android/pull/6547
dotnet/maui#4859
https://github.com/dotnet/maui/pull/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了解關于這個改進的詳細信息。
mmap系統調用
https://man7.org/linux/man-pages/man2/mmap.2.html
mmap
https://man7.org/linux/man-pages/man2/mmap.2.html
程序集存儲讀取器
https://github.com/xamarin/xamarin-android/tree/main/tools/assembly-store-reader
ILSpy
https://github.com/icsharpcode/ILSpy
xamarin-android#6311
https://github.com/xamarin/xamarin-android/pull/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在那時還不存在,但我們現在可以使用它!這提高了任何繼承Java類的c#類的成本,因此這是一個比.NET MAUI更廣泛的改進。
你可能會問,“為什么要使用字符串呢?”使用Java數組似乎比分隔字符串對性能的影響更大。在我們的測試中,調用JNI來獲取Java數組元素,性能比字符串差。Split和Span的新用法。對于如何在未來的.NET版本中重新構建它,我們有一些想法。
除了.NET 6之外,針對當前客戶Xamarin. Android的最新版本也附帶了這一更改。
查看xamarin-android#6708了解關于此改進的詳細信息。
Java本機接口(JNI)
https://en.wikipedia.org/wiki/Java_Native_Interface
Span
https://docs.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
xamarin-android#6708
https://github.com/xamarin/xamarin-android/pull/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了解這個改進的詳細信息。
xamarin-android#6766
https://github.com/xamarin/xamarin-android/pull/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 assemblycase 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 methodsinternal 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了解這個改進的詳細信息。
xamarin-android#6657
https://github.com/xamarin/xamarin-android/pull/6657
xamarin- android #6707
https://github.com/xamarin/xamarin-android/pull/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在?xamarin-android#6812
AndroidCodegenTarget
https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#androidcodegentarget
java.interop
https://github.com/xamarin/Java.Interop/commit/d9b43b52a2904e00b74b96c82a7c62c6a0c214ca
xamarin-android#6805
https://github.com/xamarin/xamarin-android/pull/6805
xamarin-android#6812
https://github.com/xamarin/xamarin-android/pull/6812
多維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 arrayJNIEnv.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了解這個改進的詳細信息。
dotnet/maui#5654
https://github.com/dotnet/maui/pull/5654
xamarin-android#6870
https://github.com/xamarin/xamarin-android/pull/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。
Glide
https://github.com/bumptech/glide
glidex.forms
https://github.com/jonathanpeppers/glidex
dotnet/maui#759
https://github.com/dotnet/maui/pull/759
dotnet/maui#5198
https://github.com/dotnet/maui/pull/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 void setFooAndBar(int foo, int bar)
{setFoo(foo);setBar(bar);
.NET MAUI視圖本質上是c#對象,有很多屬性需要在Java中以完全相同的方式設置。如果我們將這個概念應用到.NET MAUI中的每個android View中,我們可以創建一個~18參數的方法用于View創建。后續的屬性更改可以直接調用標準的android api。
對于非常簡單的.NET MAUI控件來說,這在性能上有了顯著的提高:
方法 | 平均 | 錯誤 | 標準差 | 0代 | 已分配 |
Border(Before) | 323.2 μs | 0.82 μs | 323.2? | 0.9766 | 5 KB |
Border(After) | 242.3 μs | 1.34 μs | 1.25 μs | 0.9766 | 5 KB |
CollectionView(Before) | 354.6 μs | 2.61 μs | 2.31 μs | 1.4648 | 6 KB |
CollectionView(After) | 258.3 μs | 0.49 μs | 0.43 μs | 1.4648 | 6 KB |
請參閱dotnet/maui#3372了解有關此改進的詳細信息。
dotnet/maui#3372
https://github.com/dotnet/maui/pull/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#更差:
方法 | 方法 | 錯誤 | 標準差 | 已分配 |
Java | 338.4 μs | 4.21 μs | 3.52 μs | 744 B |
CSharp | 410.2 μs | 7.92 μs | 6.61 μs | 1,336 B |
XML | 490.0 μs | 7.77 μs | 7.27 μs | 2,321 B |
接下來,我們將BenchmarkDotNet配置為單次運行,以更好地模擬啟動時發生的情況:
方法 | 中值 |
Java | 4.619 ms |
CSharp | 37.337 ms |
XML | 39.364 ms |
我們在.NET MAUI中看到了一個更簡單的布局,底部標簽導航:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><FrameLayoutandroid: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.BottomNavigationViewandroid: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了解這些改進的詳細信息
dotnet/maui#5424
https://github.com/dotnet/maui/pull/5424
dotnet/maui#5493
https://github.com/dotnet/maui/pull/5493
dotnet/maui#5528
https://github.com/dotnet/maui/pull/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。
.NET通用主機
https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host
dotnet/maui#4505
https://github.com/dotnet/maui/pull/4505
dotnet/maui#4545
https://github.com/dotnet/maui/pull/4545
在啟動時減少Shell初始化
Xamarin. Forms Shell是跨平臺應用程序導航的一種模式。這個模式是在.NET MAUI中提出的,它被推薦作為構建應用程序的默認方式。
當我們發現在啟動時使用Shell的成本(對于Xamarin和Xamarin.form和.NET MAUI),我們找到了幾個可以優化的地方:
不要在啟動時解析路由——要等到一個需要它們的導航發生。
如果沒有為導航提供查詢字符串,則只需跳過處理查詢字符串的代碼。這將刪除過度使用System.Reflection的代碼路徑。
如果頁面沒有可見的BottomNavigationView,那么不要設置菜單項或任何外觀元素。
請參閱dotnet/maui#5262了解此改進的詳細信息。
Xamarin. Forms Shell
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/
dotnet/maui#5262
https://github.com/dotnet/maui/pull/5262
字體不應該使用臨時文件
大量的時間花在.NET MAUI應用程序加載字體上:
32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3<string, Microsoft.Maui.FontWeight, bool>)
檢查代碼時,它所做的工作比需要的更多:
將androidAsset文件保存到臨時文件夾。
使用android API, Typeface.CreateFromFile()來加載文件。
我們實際上可以直接使用Typeface.CreateFromAsset() android API,根本不用臨時文件。
請參閱dotnet/maui#4933了解有關此改進的詳細信息。
dotnet/maui#4933
https://github.com/dotnet/maui/pull/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。
dotnet/maui#4829
https://github.com/dotnet/maui/pull/4829
dotnet/maui#5611
https://github.com/dotnet/maui/pull/5611
在XAML中使用編譯轉換器
以下類型現在在XAML編譯時轉換,而不是在運行時:
顏色:dotnet /maui# 4687
https://github.com/dotnet/maui/pull/4687
角半徑:?dotnet / maui # 5192
https://github.com/dotnet/maui/pull/5192
字形大小:dotnet / maui # 5338
https://github.com/dotnet/maui/pull/5338
網格長度, 行定義, 列定義:dotnet/maui#5489
https://github.com/dotnet/maui/pull/5489
這導致從.xaml文件生成更好/更快的IL。
優化顏色解析
Microsoft.Maui.Graphics.Color.Parse()的原始代碼可以重寫,以更好地使用Span并避免字符串分配。
方法 | 平均 | 錯誤 | 標準差 | 0代 | 已分配 |
Parse (之前) | 99.13 ns | 0.281 ns | 0.235 ns | 0.0267 | 168 B |
Parse (之后) | 52.54 ns | 0.292 ns | 0.259 ns | 0.0051 | 32 B |
能夠在ReadonlySpan<char>dotnet/csharplang#1881上使用switch語句,將在未來的.NET版本中進一步改善這種情況。
看到dotnet / Microsoft.Maui.Graphics # 343和dotnet / Microsoft.Maui.Graphics # 345關于這個改進的細節。
dotnet/csharplang#1881
https://github.com/dotnet/csharplang/issues/1881
dotnet / Microsoft.Maui.Graphics # 343
https://github.com/dotnet/Microsoft.Maui.Graphics/pull/343
dotnet / Microsoft.Maui.Graphics # 345
https://github.com/dotnet/Microsoft.Maui.Graphics/pull/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"))
{// do something
}
如果在整個應用程序中執行,System.Globalization.CultureInfo.CurrentCulture可以避免被調用,并且可以稍微提高If語句的總體速度。
為了解決整個dotnet/maui回購的這種情況,我們引入了代碼分析規則來捕捉這些:
dotnet_diagnostic.CA1307.severity = error
dotnet_diagnostic.CA1309.severity?=?error
請參閱dotnet/maui#4988了解有關改進的詳細信息。
dotnet/maui#4988
https://github.com/dotnet/maui/pull/4988
懶惰地創建日志
ConfigureFonts() API在啟動時花費了一些時間來做一些可以延遲到以后的工作。我們還可以改進Microsoft.Extensions中日志基礎設施的一般用法。
我們所做的一些改進如下:
推遲創建“記錄器”類,直到需要它們時再創建。
內置的日志記錄基礎設施在默認情況下是禁用的,必須顯式啟用。
延遲調用android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。
不要使用ILoggerFactory創建通用記錄器。而是直接獲取ILogger服務,這樣它就被緩存了。
請參閱dotnet/maui#5103了解有關此改進的詳細信息。
dotnet/maui#5103
https://github.com/dotnet/maui/pull/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了解有關此改進的詳細信息。
bannedapianalyzer
https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md
dotnet/maui#5290
https://github.com/dotnet/maui/pull/5290
默認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了解關于這個改進的詳細信息。
.NET Podcast
https://github.com/dotnet/runtime/pull/65326
xamarin-android#6727
https://github.com/xamarin/xamarin-android/pull/6727
xamarin-macios#14130
https://github.com/xamarin/xamarin-macios/pull/14130
懶惰地負載ConfigurationManager
configurationmanager并沒有被許多移動應用程序使用,而且創建一個是非常昂貴的!(例如,在android上約為7.59ms)
在.NET MAUI中,一個ConfigurationManager在啟動時默認被創建,我們可以使用Lazy延遲它的創建,所以它將不會被創建,除非請求。
請參閱dotnet/maui#5348了解有關此改進的詳細信息。
dotnet/maui#5348
https://github.com/dotnet/maui/pull/5348
改進內置AOT配置文件
Mono運行時有一個關于每個方法的JIT時間的報告(參見我們的文檔),例如:
Total(ms) | Self(ms) | Method3.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報告:
_ = 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}"?/>
這導致了進一步的補充:
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版本中完全支持記錄自定義概要文件。
參見我們的文檔
https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/profiling.md#profiling-the-jit-compiler
.NET Podcast
https://github.com/microsoft/dotnet-podcasts
dotnet/maui#5559
https://github.com/dotnet/maui/pull/5559
dotnet/maui#5682
https://github.com/dotnet/maui/pull/5682
dotnet/maui#6834
https://github.com/dotnet/maui/pull/6834
Mono.Profiler.Android
https://github.com/jonathanpeppers/Mono.Profiler.Android
啟用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了解這些改進的詳細信息。
dotnet/runtime#67024
https://github.com/dotnet/runtime/pull/67024
xamarin-android #6940
https://github.com/xamarin/xamarin-android/pull/6940
刪除System.Uri中未使用的編碼對象
一個MAUI應用程序的dotnet跟蹤輸出,顯示大約7ms花費了加載UTF32和Latin1編碼的第一次系統。使用Uri api:
<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>
這個字段是不小心留在原地的。只需刪除s_noFallbackCharUTF8字段,就可以改進任何使用System.Uri 或相關的api的. net應用程序的啟動。
參見dotnet/runtime#65326了解有關此改進的詳細信息。
dotnet/runtime#65326
https://github.com/dotnet/runtime/pull/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-xxxhdpiappiconfg.png = 1824x1824dotnet_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 = 512x512dotnet_bot.png?=?672x832
我們還可以修改.svg內容,但這可能不可取,這取決于圖形設計師如何在其他設計工具中使用該圖像。
在另一個例子中,一個3008×5340 .jpg圖像:
<MauiImage Include="Resources\Images\large.jpg" />
正在升級到21360×12032!設置Resize="false"將防止圖像被調整大小,但我們將此設置為非矢量圖像的默認選項。接下來,開發人員應該能夠依賴默認值,或者根據需要指定%(基本尺寸)和%(調整大小)。
這些改變改善了啟動性能和應用程序的大小。請參閱dotnet/maui#4759和dotnet/maui#6419了解這些改進的細節。
dotnet/maui#4759
https://github.com/dotnet/maui/pull/4759
dotnet/maui#6419
https://github.com/dotnet/maui/pull/6419
刪除Application.Properties 和DataContractSerializer
Xamarin.Forms 有一個 API,用于通過 Application.Properties 字典持久化鍵值對。這在內部使用了DataContractSerializer,這對于自包含和修剪的移動應用程序不是最佳選擇。來自BCL的System.Xml的部分可能相當大,我們不想在每個.NET MAUI應用程序中都為此付出代價。
簡單地刪除這個API和所有DataContractSerializer的使用,在android上可以提高約855KB,在iOS上提高約1MB。
請參閱dotnet/maui#4976了解有關此改進的詳細信息。
dotnet/maui#4976
https://github.com/dotnet/maui/pull/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關于這個改進的詳細信息。
dotnet/runtime#64852
https://github.com/dotnet/runtime/pull/64852
xamarin-android#6749
https://github.com/xamarin/xamarin-android/pull/6749
xamarin-macios#14297
https://github.com/xamarin/xamarin-macios/pull/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了解有關改進的詳細信息。
dotnet/runtime#66863
https://github.com/dotnet/runtime/issues/66863
dotnet podcasts#44
https://github.com/microsoft/dotnet-podcasts/pull/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了解有關改進的詳細信息。
.NET Podcast?
https://github.com/microsoft/dotnet-podcasts
MonkeyCache
https://github.com/jamesmontemagno/monkey-cache
monkey-cache#109
https://github.com/jamesmontemagno/monkey-cache/pull/109
dotnet-podcasts#58
https://github.com/microsoft/dotnet-podcasts/pull/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了解有關此改進的詳細信息。
dotnet-podcasts#57
https://github.com/microsoft/dotnet-podcasts/pull/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 filepublic 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* finalpublic int foo = -1;}
}
然后主應用程序就會有如下代碼:
public class Resource
{public class Drawable{public Drawable()
{// Copy the value at runtimeglobal::MyLibrary.Resource.Drawable.foo = foo;}// The actual integer here *is* finalpublic 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了解該功能的詳細信息。
dotnet/maui#2606
https://github.com/dotnet/maui/pull/2606
xamarin-android#5317
https://github.com/xamarin/xamarin-android/pull/5317
xamarin-android#6696
https://github.com/xamarin/xamarin-android/pull/6696
dotnet/maui#4912
https://github.com/dotnet/maui/pull/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文檔。
我們的D8/R8文檔
https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/D8andR8.md
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
https://llvm.org/
EnableLLVM的文檔
https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties
記錄自定義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版本中完全支持記錄自定義概要文件。
Mono.Profiler.Android
https://github.com/jonathanpeppers/Mono.Profiler.Android
結論
我希望您喜歡我們的.NET MAUI性能論述。請嘗試.NET MAUI并且可以在http://dot.net/maui了解更多!