.NET MAUI 性能提升(上)

.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/?ocid=AID3045631

  • .NET Podcast App (Shell):

    https://github.com/microsoft/dotnet-podcasts

主要内容

❖ 启动性能的改进
  • 在移动设备上进行分析

  • 测量随着时间的推移

  • 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诊断工具,因为它是我们使.NET MAUI更快的第0步。
分析.NET 6 android应用程序需要使用一个叫做dotnet-dsrouter的工具。该工具使dotnet跟踪连接到一个运行的移动应用程序在android, iOS等。这可能是我们用来分析.NET MAUI的最有影响力的工具。
要开始使用dotnet trace和dsrouter,首先通过adb配置一些设置并启动dsrouter:
adb reverse tcp:9000 tcp:9001adb 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 性能提升(上)

.NET MAUI 性能提升(上)

在android应用程序中使用dotnet跟踪的更多细节,请参阅我们的文档。我建议在android设备上分析Release版本,以获得应用在现实世界中的最佳表现。

  • 在移动设备上进行分析:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiling-on-mobile?ocid=AID3045631
  • dotnet-dsrouter:
    https://docs.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dsrouter?ocid=AID3045631
  • 我们的文档:

    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模板的启动时间(以毫秒为单位)图:
.NET MAUI 性能提升(上)
注意,Pixel 4a比Pixel 5要慢得多。
我们可以精确地指出在dotnet/maui中发生的回归和改进。这对于追踪我们的目标是非常有用的。
同样地,我们可以在相同的Pixel 4a设备上看到.NET Podcast应用随着时间的推移所取得的进展:

.NET MAUI 性能提升(上)

这张图表是我们真正关注的焦点,因为它是一款“真正的应用”,接近于开发者在自己的手机应用中看到的内容。
至于应用程序大小,它是一个更稳定的数字——当情况变得更糟或更好时,它很容易归零:

.NET MAUI 性能提升(上)

请参阅dotnet-podcasts#58, Android x# 520和dotnet/maui#6419了解这些改进的详细信息。

  • 测量随着时间的推移:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#measuring-over-time?ocid=AID3045631

  • .NET Podcas:
    https://github.com/microsoft/dotnet-podcasts
  • dotnet-podcasts#58:
    https://github.com/microsoft/dotnet-podcasts/pull/58
  • 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了解这个改进的细节。

  • Profiled AOT:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiled-aot?ocid=AID3045631

  • 启动跟踪或分析AOT:

    https://devblogs.microsoft.com/xamarin/faster-startup-times-with-startup-tracing-on-android/?ocid=AID3045631

  • 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.dllassemblies/Mono.android.dllassemblies/System.Runtime.dllassemblies/arm64-v8a/System.Private.CoreLib.dllassemblies/armeabi-v7a/System.Private.CoreLib.dllassemblies/x86/System.Private.CoreLib.dllassemblies/x86_64/System.Private.CoreLib.dll
这些文件是通过mmap系统调用单独加载的,这是应用程序中每个.NET程序集的成本。这是在android工作负载中用C/ c++实现的,使用Mono运行时为程序集加载提供的回调。MAUI应用程序有很多程序集,所以我们引入了一个新的$(androidUseAssemblyStore)特性,该特性在Release版本中默认启用。
在这个改变之后,你会得到:
assemblies/assemblies.manifestassemblies/assemblies.blobassemblies/assemblies.arm64_v8a.blobassemblies/assemblies.armeabi_v7a.blobassemblies/assemblies.x86.blobassemblies/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了解关于这个改进的详细信息。

  • 单文件程序集存储器:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores

  • 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<T>在那时还不存在,但我们现在可以使用它!这提高了任何继承Java类的c#类的成本,因此这是一个比.NET MAUI更广泛的改进。

你可能会问,“为什么要使用字符串呢?”使用Java数组似乎比分隔字符串对性能的影响更大。在我们的测试中,调用JNI来获取Java数组元素,性能比字符串差。Split和Span的新用法。对于如何在未来的.NET版本中重新构建它,我们有一些想法。

除了.NET 6之外,针对当前客户Xamarin. Android的最新版本也附带了这一更改。
查看xamarin-android#6708了解关于此改进的详细信息。
  • Spanify.RegisterNativeMembers:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#spanify-registernativemembers?ocid=AID3045631

  • 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?ocid=AID3045631
  • xamarin-android#6708:

    https://github.com/xamarin/xamarin-android/pull/6708https://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了解这个改进的详细信息。
  • System.Reflection.Emit和构造函数:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-constructors?ocid=AID3045631

  • 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 assemblyinternal 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了解这个改进的详细信息。

  • System.Reflection.Emit和方法:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-methods?ocid=AID3045631

  • 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<T>在 xamarin-android#6812
  • 更新的Java.Interop APIs:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays?ocid=AID3045631
  • $(AndroidCodegenTarget):
    https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#androidcodegentarget?ocid=AID3045631
  • 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 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 frommonodroid-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了解这个改进的详细信息。

  • 多维Java数组:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays?ocid=AID3045631
  • 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。

  • 为android图像使用Glide:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays?ocid=AID3045631
  • 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 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控件来说,这在性能上有了显著的提高:
方法
平均
错误
标准差
0
已分配
Border(Before)
323.2  µs
0.82 µs
0.68  µs
0.9766
5KB
Border(After)
242.3  µs
1.34 µs
1.25  µs
0.9766
5KB
CollectionView(Before)
354.6  µs
2.61 µs
2.31  µs
1.4648
6KB
CollectionView(After)
258.3  µs
0.49 µs
0.43  µs
1.4648
6KB

请参阅dotnet/maui#3372了解有关此改进的详细信息。

  • 减少Java互操作调用:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#reduce-java-interop-calls?ocid=AID3045631
  • 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"?><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方法中,例如:
@NonNullpublic static List<View> createBottomTabLayout(Context context, int navigationStyle);@NonNullpublic static LinearLayout createLinearLayout(Context context);@NonNullpublic static FrameLayout createFrameLayout(Context context, LinearLayout layout);@NonNullpublic 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了解这些改进的详细信息。

  • 将android XML移植到Java:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#port-android-xml-to-java?ocid=AID3045631

  • 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。

  • 删除Microsoft.Extensions.Hosting:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshosting?ocid=AID3045631

  • .NET通用主机:

    https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host?ocid=AID3045631

  • 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了解此改进的详细信息。

  • 在启动时减少Shell初始化:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#less-shell-initialization-on-startup?ocid=AID3045631

  • Xamarin. Forms Shell:

    https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/?ocid=AID3045631

  • 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>)
检查代码时,它所做的工作比需要的更多:
1.将androidAsset文件保存到临时文件夹。
2.使用android API, Typeface.CreateFromFile()来加载文件。
我们实际上可以直接使用Typeface.CreateFromAsset() android API,根本不用临时文件。

请参阅dotnet/maui#4933了解有关此改进的详细信息。

  • 字体不应该使用临时文件:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fonts-should-not-use-temporary-files?ocid=AID3045631
  • 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。

  • 编译时在平台上计算:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#compute-onplatform-at-compile-time?ocid=AID3045631
  • 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
  • 角半径: dotnet / maui # 5192
  • 字形大小:dotnet / maui # 5338
  • 网格长度, 行定义, 列定义: dotnet/maui#5489
这导致从.xaml文件生成更好/更快的IL。
  • 在XAML中使用编译转换器:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-compiled-converters-in-xaml?ocid=AID3045631
  • 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
▌优化颜色解析
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关于这个改进的细节。

  • 优化颜色解析:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#optimize-color-parsing?ocid=AID3045631
  • 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.GetNavigationState3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri3.82ms System.Private.CoreLib!System.String.StartsWith2.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 = errordotnet_diagnostic.CA1309.severity = error

请参阅dotnet/maui#4988了解有关改进的详细信息。

  • 不要使用区域性识别的字符串比较:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#dont-use-culture-aware-string-comparisons?ocid=AID3045631
  • dotnet/maui#4988:

    https://github.com/dotnet/maui/pull/4988
▌懒惰地创建日志
ConfigureFonts() API在启动时花费了一些时间来做一些可以延迟到以后的工作。我们还可以改进Microsoft.Extensions中日志基础设施的一般用法。
我们所做的一些改进如下:
  • 推迟创建“记录器”类,直到需要它们时再创建。
  • 内置的日志记录基础设施在默认情况下是禁用的,必须显式启用。
  • 延迟调用android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。
  • 不要使用ILoggerFactory创建通用记录器。而是直接获取ILogger服务,这样它就被缓存了。

请参阅dotnet/maui#5103了解有关此改进的详细信息。

  • 懒惰地创建日志:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily?ocid=AID3045631

  • 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 dependenciesservices.TryAddSingleton<IFooService>(sp => new FooService());// Or if you need to retrieve some dependenciesservices.TryAddSingleton<IFooService>(sp => new FooService(sp.GetService<IBar>()));
在这种情况下,Microsoft.Extensions可以简单地调用lamdba/匿名方法,而不需要系统。反射。
我们在所有的dotnet/maui上进行了改进,并使用了bannedapianalyzer,这样就不会有人意外地使用TryAddSingleton()更慢的重载。

请参阅dotnet/maui#5290了解有关此改进的详细信息。

  • 使用工厂方法进行依赖注入:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-factory-methods-for-dependency-injection?ocid=AID3045631
  • 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
▌懒惰地负载ConfigurationManager

configurationmanager并没有被许多移动应用程序使用,而且创建一个是非常昂贵的!(例如,在android上约为7.59ms)

在.NET MAUI中,一个ConfigurationManager在启动时默认被创建,我们可以使用Lazy延迟它的创建,所以它将不会被创建,除非请求。

请参阅dotnet/maui#5348了解有关此改进的详细信息。

  • 懒惰地负载ConfigurationManager:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#load-configurationmanager-lazily?ocid=AID3045631

  • dotnet/maui#5348:

    https://github.com/dotnet/maui/pull/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了解关于这个改进的详细信息。

  • 默认VerifyDependencyInjectionOpenGenericServiceTrimmability:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#default-verifydependencyinjectionopengenericservicetrimmability?ocid=AID3045631

  • .NET Podcast:

    https://github.com/microsoft/dotnet-podcasts

  • xamarin-android#6727:

    https://github.com/xamarin/xamarin-android/pull/6727

  • xamarin-macios#14130:

    https://github.com/xamarin/xamarin-macios/pull/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配置文件:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#improve-the-built-in-aot-profile?ocid=AID3045631

  • 参见我们的文档:

    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了解这些改进的详细信息。

  • 启用AOT图像的延迟加载:
    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#enable-lazy-loading-of-aot-images?ocid=AID3045631
  • 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:
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了解有关此改进的详细信息。

  • 删除System.Uri中未使用的编码对象:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-unused-encoding-object-in-systemuri?ocid=AID3045631

  • dotnet/runtime#65326:

    https://github.com/dotnet/runtime/pull/65326