• .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示例中的改进

    实验性或高级选项

    启动性能的改进

    在移动设备上进行分析

    我必须提到移动平台上可用的.NET诊断工具,因为它是我们使.NET MAUI更快的第0步。

    分析.NET 6 android应用程序需要使用一个叫做dotnet-dsrouter的工具。该工具使dotnet跟踪连接到一个运行的移动应用程序在android, iOS等。这可能是我们用来分析.NET MAUI的最有影响力的工具。

    要开始使用dotnet trace和dsrouter,首先通过adb配置一些设置并启动dsrouter:

    1. adb reverse tcp:9000 tcp:9001
    2. adb shell setprop debug.mono.profile '127.0.0.1:9000,suspend'
    3. dotnet-dsrouter client-server -tcps 127.0.0.1:9001 -ipcc /tmp/maui-app --verbose debug

    下一步启动dotnet跟踪,如:

    dotnet-trace collect --diagnostic-port /tmp/maui-app --format speedscope

    在启动一个使用-c Release和-p:androidEnableProfiler=true构建的android应用程序后,当dotnet trace输出时,你会注意到连接:

    Press <Enter> or <Ctrl+C> to exit...812  (KB)

    在您的应用程序完全启动后,只需按下enter键就可以得到一个保存在当前目录的*.speedscope。你可以在https://speedscope.app上打开这个文件,深入了解每个方法在应用程序启动期间所花费的时间:

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

    测量随着时间的推移

    我们在.NET基础团队的朋友建立了一个管道来跟踪.NET MAUI性能场景,例如:

    • 包大小
    • 磁盘大小(未压缩)
    • 单个文件分类
    • 应用程序启动

    随着时间的推移,这使我们能够看到改进或回归的影响,看到dotnet/maui回购的每个提交的数字。我们还可以确定这种差异是否是由xamarin-android、xamarin-macios或dotnet/runtime中的变化引起的。

    例如,在物理Pixel 4a设备上运行的dotnet new maui模板的启动时间(以毫秒为单位)图:

    注意,Pixel 4a比Pixel 5要慢得多。

    我们可以精确地指出在dotnet/maui中发生的回归和改进。这对于追踪我们的目标是非常有用的。

    同样地,我们可以在相同的Pixel 4a设备上看到.NET Podcast应用随着时间的推移所取得的进展:

    这张图表是我们真正关注的焦点,因为它是一款“真正的应用”,接近于开发者在自己的手机应用中看到的内容。

    至于应用程序大小,它是一个更稳定的数字——当情况变得更糟或更好时,它很容易归零:

    请参阅dotnet-podcasts#58, Android x# 520dotnet/maui#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#6547dotnet/maui#4859了解这个改进的细节。

    单文件程序集存储器

    之前,如果你在你最喜欢的zip文件实用程序中查看Release android .apk内容,你可以看到.NET程序集位于:

    1. assemblies/Java.Interop.dll
    2. assemblies/Mono.android.dll
    3. assemblies/System.Runtime.dll
    4. assemblies/arm64-v8a/System.Private.CoreLib.dll
    5. assemblies/armeabi-v7a/System.Private.CoreLib.dll
    6. assemblies/x86/System.Private.CoreLib.dll
    7. assemblies/x86_64/System.Private.CoreLib.dll

    这些文件是通过mmap系统调用单独加载的,这是应用程序中每个.NET程序集的成本。这是在android工作负载中用C/ c++实现的,使用Mono运行时为程序集加载提供的回调。MAUI应用程序有很多程序集,所以我们引入了一个新的$(androidUseAssemblyStore)特性,该特性在Release版本中默认启用。

    在这个改变之后,你会得到:

    1. assemblies/assemblies.manifest
    2. assemblies/assemblies.blob
    3. assemblies/assemblies.arm64_v8a.blob
    4. assemblies/assemblies.armeabi_v7a.blob
    5. assemblies/assemblies.x86.blob
    6. 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包装器,例如:

    1. public class MainActivity extends Android.app.Activity
    2. {
    3.     public static final String methods;
    4.     static {
    5.         methods = "n_onCreate:(LAndroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n";
    6.         mono.Android.Runtime.register ("foo.MainActivity, foo", MainActivity.class, methods);
    7.     }

    方法列表是一个以\n和:分隔的Java本机接口(JNI)签名列表,这些签名在托管的c#代码中被重写。对于在c#中重写的每个Java方法,您都会得到一个这样的方法。

    当实际的Java onCreate()方法被调用为一个android活动:

    1. public void onCreate (Android.os.Bundle p0)
    2. {
    3.     n_onCreate (p0);
    4. }
    5. 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#构造函数的有点复杂的方法。

    首先,我们有一些在启动时发生的反射调用:

    1. static MethodInfo newobject = typeof (System.Runtime.CompilerServices.RuntimeHelpers).GetMethod ("GetUninitializedObject", BindingFlags.Public | BindingFlags.Static)!;
    2. static MethodInfo gettype = typeof (System.Type).GetMethod ("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static)!;
    3. static FieldInfo handle = typeof (Java.Lang.Object).GetField ("handle", BindingFlags.NonPublic | BindingFlags.Instance)!;

    这似乎是Mono早期版本遗留下来的,并一直延续到今天。例如,可以直接调用RuntimeHelpers.GetUninitializedObject()。

    然后是一些复杂的System.Reflection.Emit用法,并在System.Reflection.ConstructorInfo中传递一个cinfo实例:

    1. DynamicMethod method = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), typeof (void), new Type [] {typeof (IntPtr), typeof (object []) }, typeof (DynamicMethodNameCounter), true);
    2. ILGenerator il = method.GetILGenerator ();
    3. il.DeclareLocal (typeof (object));
    4. il.Emit (OpCodes.Ldtoken, type);
    5. il.Emit (OpCodes.Call, gettype);
    6. il.Emit (OpCodes.Call, newobject);
    7. il.Emit (OpCodes.Stloc_0);
    8. il.Emit (OpCodes.Ldloc_0);
    9. il.Emit (OpCodes.Ldarg_0);
    10. il.Emit (OpCodes.Stfld, handle);
    11. il.Emit (OpCodes.Ldloc_0);
    12. var len = cinfo.GetParameters ().Length;
    13. for (int i = 0; i < len; i++) {
    14.     il.Emit (OpCodes.Ldarg, 1);
    15.     il.Emit (OpCodes.Ldc_I4, i);
    16.     il.Emit (OpCodes.Ldelem_Ref);
    17. }
    18. il.Emit (OpCodes.Call, cinfo);
    19. il.Emit (OpCodes.Ret);
    20. return (Action<IntPtr, object?[]?>) method.CreateDelegate (typeof (Action <IntPtr, object []>));

    调用返回的委托,使得IntPtr是Java.Lang.Object子类的句柄,而对象[]是该特定c#构造函数的任何参数。emit对于在启动时第一次使用它以及以后的每次调用都有很大的成本。

    经过仔细的审查,我们可以将handle字段设置为内部的,并将此代码简化为:

    1. var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType);
    2. if (newobj is Java.Lang.Object o) {
    3.     o.handle = jobject;
    4. } else if (newobj is Java.Lang.Throwable throwable) {
    5.     throwable.handle = jobject;
    6. } else {
    7.     throw new InvalidOperationException ($"Unsupported type: '{newobj}'");
    8. }
    9. 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方法时,比如:

    1. public class MainActivity : Activity
    2. {
    3.     protected override void OnCreate(Bundle savedInstanceState)
    4.     {
    5.          base.OnCreate(savedInstanceState);
    6.          //...
    7.     }
    8. }

    在从Java到c#的转换过程中,我们必须封装c#方法来处理异常,例如:

    1. try
    2. {
    3.     // Call the actual C# method here
    4. }
    5. catch (Exception e) when (_unhandled_exception (e))
    6. {
    7.     androidEnvironment.UnhandledException (e);
    8.     if (Debugger.IsAttached || !JNIEnv.PropagateExceptions)
    9.         throw;
    10. }

    例如,如果在OnCreate()中未处理托管异常,那么实际上会导致本机崩溃(并且没有托管的c#堆栈跟踪)。我们需要确保调试器在附加异常时能够中断,否则将记录c#堆栈跟踪。

    从Xamarin开始,上面的代码是通过System.Reflection.Emit生成的:

    1. var dynamic = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), ret_type, param_types, typeof (DynamicMethodNameCounter), true);
    2. var ig = dynamic.GetILGenerator ();
    3. LocalBuilder? retval = null;
    4. if (ret_type != typeof (void))
    5.     retval = ig.DeclareLocal (ret_type);
    6. ig.Emit (OpCodes.Call, wait_for_bridge_processing_method!);
    7. var label = ig.BeginExceptionBlock ();
    8. for (int i = 0; i < param_types.Length; i++)
    9.     ig.Emit (OpCodes.Ldarg, i);
    10. ig.Emit (OpCodes.Call, dlg.Method);
    11. if (retval != null)
    12.     ig.Emit (OpCodes.Stloc, retval);
    13. ig.Emit (OpCodes.Leave, label);
    14. bool  filter = Debugger.IsAttached || !JNIEnv.PropagateExceptions;
    15. if (filter && JNIEnv.mono_unhandled_exception_method != null) {
    16.     ig.BeginExceptFilterBlock ();
    17.     ig.Emit (OpCodes.Call, JNIEnv.mono_unhandled_exception_method);
    18.     ig.Emit (OpCodes.Ldc_I4_1);
    19.     ig.BeginCatchBlock (null!);
    20. } else {
    21.     ig.BeginCatchBlock (typeof (Exception));
    22. }
    23. ig.Emit (OpCodes.Dup);
    24. ig.Emit (OpCodes.Call, exception_handler_method!);
    25. if (filter)
    26.     ig.Emit (OpCodes.Throw);
    27. ig.EndExceptionBlock ();
    28. if (retval != null)
    29.     ig.Emit (OpCodes.Ldloc, retval);
    30. ig.Emit (OpCodes.Ret);

    这段代码被调用两次为一个 dotnet new android 应用程序,但~58次为一个dotnet new maui应用程序!

    我们意识到实际上可以为每个通用委托类型编写一个强类型的“快速路径”,而不是使用System.Reflection.Emit。有一个生成的委托匹配每个签名:

    1. void OnCreate(Bundle savedInstanceState);
    2. // Maps to *JNIEnv, JavaClass, Bundle
    3. // Internal to each assembly
    4. internal delegate void _JniMarshal_PPL_V(IntPtr, IntPtr, IntPtr);

    这样我们就可以列出所有使用过的dotnet maui应用程序的签名,比如:

    1. class JNINativeWrapper
    2. {
    3. static Delegate? CreateBuiltInDelegate (Delegate dlg, Type delegateType)
    4. {
    5. switch (delegateType.Name)
    6. {
    7. // Unsafe.As<T>() is used, because _JniMarshal_PPL_V is generated internal in each assembly
    8. case nameof (_JniMarshal_PPL_V):
    9. return new _JniMarshal_PPL_V (Unsafe.As<_JniMarshal_PPL_V> (dlg).Wrap_JniMarshal_PPL_V);
    10. // etc.
    11. }
    12. return null;
    13. }
    14. // Static extension method is generated to avoid capturing variables in anonymous methods
    15. internal static void Wrap_JniMarshal_PPL_V (this _JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
    16. {
    17. // ...
    18. }
    19. }

    这种方法的缺点是,当使用新签名时,我们必须列出更多的情况。不想详尽地列出每一种组合,因为这会导致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文档获取关于该特性的历史记录。

    其他有问题的地方是有手动绑定的地方。这些往往也是经常使用的方法,所以值得修复这些!

    一些改善这种情况的例子:

    多维Java数组

    当向Java来回传递c#数组时,中间步骤必须复制数组,以便适当的运行时能够访问它。这真的是一个开发者体验的情况,因为c#开发者期望写这样的东西:

    1. var array = new int[] { 1, 2, 3, 4};
    2. MyJavaMethod (array);
    3. 在MyJavaMethod里面会做:
    4. IntPtr native_items = JNIEnv.NewArray (items);
    5. try
    6. {
    7. // p/invoke here, actually calls into Java
    8. }
    9. finally
    10. {
    11. if (items != null)
    12. {
    13. JNIEnv.CopyArray (native_items, items); // If the calling method mutates the array
    14. JNIEnv.DeleteLocalRef (native_items); // Delete our Java local reference
    15. }
    16. }

    JNIEnv.NewArray()访问一个类型映射,以知道需要将哪个Java类用于数组的元素。

    dotnet new maui项目使用的特定android API有问题:

    public ColorStateList (int[][]? states, int[]? colors)

    发现一个多维 int[][] 数组可以访问每个元素的“类型映射”。 当启用额外的日志记录时,我们可以看到这一点,许多实例:

    1. 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)
    2. monodroid-assembly: typemap: called from
    3. monodroid-assembly: at android.Runtime.JNIEnv.TypemapManagedToJava(Type )
    4. monodroid-assembly: at android.Runtime.JNIEnv.GetJniName(Type )
    5. monodroid-assembly: at android.Runtime.JNIEnv.FindClass(Type )
    6. monodroid-assembly: at android.Runtime.JNIEnv.NewArray(Array , Type )
    7. monodroid-assembly: at android.Runtime.JNIEnv.NewArray[Int32[]](Int32[][] )
    8. monodroid-assembly: at android.Content.Res.ColorStateList..ctor(Int32[][] , Int32[] )
    9. 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编写的,例如:

    1. import com.bumptech.glide.Glide;
    2. //...
    3. public static void loadImageFromUri(ImageView imageView, String uri, Boolean cachingEnabled, ImageLoaderCallback callback) {
    4. //...
    5. RequestBuilder<Drawable> builder = Glide
    6. .with(imageView)
    7. .load(androidUri);
    8. loadInto(builder, imageView, cachingEnabled, callback);
    9. }

    ImageLoaderCallback在c#中子类化以处理托管代码中的完成。其结果是,来自web的图像的性能应该比以前在Xamarin.Forms中得到的性能有了显著提高。

    详见dotnet/maui#759dotnet/maui#5198

    减少Java互操作调用

    假设你有以下Java api:

    1. public void setFoo(int foo);
    2. public void setBar(int bar);

    这些方法的互操作如下:

    1. public unsafe static void SetFoo(int foo)
    2. {
    3. JniArgumentValue* __args = stackalloc JniArgumentValue[1];
    4. __args[0] = new JniArgumentValue(foo);
    5. return _members.StaticMethods.InvokeInt32Method("setFoo.(I)V", __args);
    6. }
    7. public unsafe static void SetBar(int bar)
    8. {
    9. JniArgumentValue* __args = stackalloc JniArgumentValue[1];
    10. __args[0] = new JniArgumentValue(bar);
    11. return _members.StaticMethods.InvokeInt32Method("setBar.(I)V", __args);

    所以调用这两个方法会两次调用stackalloc,两次调用p/invoke。创建一个小型的Java包装器会更有性能,例如:

    1. public void setFooAndBar(int foo, int bar)
    2. {
    3. setFoo(foo);
    4. setBar(bar);
    5. }

    翻译为:

    1. public unsafe static void SetFooAndBar(int foo, int bar)
    2. {
    3. JniArgumentValue* __args = stackalloc JniArgumentValue[2];
    4. __args[0] = new JniArgumentValue(foo);
    5. __args[1] = new JniArgumentValue(bar);
    6. return _members.StaticMethods.InvokeInt32Method("setFooAndBar.(II)V", __args);
    7. }

    .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

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

    将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中看到了一个更简单的布局,底部标签导航:

    1. <?xml version="1.0" encoding="utf-8"?>
    2. <LinearLayout
    3. xmlns:android="http://schemas.android.com/apk/res/android"
    4. android:orientation="vertical"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent">
    7. <FrameLayout
    8. android:id="@+id/bottomtab.navarea"
    9. android:layout_width="match_parent"
    10. android:layout_height="0dp"
    11. android:layout_gravity="fill"
    12. android:layout_weight="1" />
    13. <com.google.android.material.bottomnavigation.BottomNavigationView
    14. android:id="@+id/bottomtab.tabbar"
    15. android:theme="@style/Widget.Design.BottomNavigationView"
    16. android:layout_width="match_parent"
    17. android:layout_height="wrap_content" />
    18. </LinearLayout>

    我们可以将其移植到四个Java方法中,例如:

    1. @NonNull
    2. public static List<View> createBottomTabLayout(Context context, int navigationStyle);
    3. @NonNull
    4. public static LinearLayout createLinearLayout(Context context);
    5. @NonNull
    6. public static FrameLayout createFrameLayout(Context context, LinearLayout layout);
    7. @NonNull
    8. 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}标记扩展的使用:

    1. <Label Text="Platform: " />
    2. <Label Text="{OnPlatform Default=Unknown, android=android, iOS=iOS" />

    …实际上可以在编译时计算,net6.0-android和net6.0-ios会得到适当的值。在未来的.NET版本中,我们将对 XML元素进行同样的优化。

    详见dotnet/maui#4829dotnet/maui#5611

    在XAML中使用编译转换器

    以下类型现在在XAML编译时转换,而不是在运行时:

    这导致从.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 # 343dotnet / Microsoft.Maui.Graphics # 345关于这个改进的细节。

    不要使用区域性识别的字符串比较

    回顾一个新的naui项目的dotnet跟踪输出,可以看到android上第一个区域性感知字符串比较的真实成本:

    1. 6.32ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellNavigationManager.GetNavigationState
    2. 3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri
    3. 3.82ms System.Private.CoreLib!System.String.StartsWith
    4. 2.57ms System.Private.CoreLib!System.Globalization.CultureInfo.get_CurrentCulture

    实际上,我们甚至不希望在本例中使用区域性比较—它只是从Xamarin.Forms引入的代码。

    例如,如果你有:

    1. if (text.StartsWith("f"))
    2. {
    3. // do something
    4. }

    在这种情况下,你可以简单地这样做:

    1. if (text.StartsWith("f", StringComparision.Ordinal))
    2. {
    3. // do something
    4. }

    如果在整个应用程序中执行,System.Globalization.CultureInfo.CurrentCulture可以避免被调用,并且可以稍微提高If语句的总体速度。

    为了解决整个dotnet/maui回购的这种情况,我们引入了代码分析规则来捕捉这些:

    1. dotnet_diagnostic.CA1307.severity = error
    2. dotnet_diagnostic.CA1309.severity = error

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

    懒惰地创建日志

    ConfigureFonts() API在启动时花费了一些时间来做一些可以延迟到以后的工作。我们还可以改进Microsoft.Extensions中日志基础设施的一般用法。

    我们所做的一些改进如下:

    • 推迟创建“记录器”类,直到需要它们时再创建。
    • 内置的日志记录基础设施在默认情况下是禁用的,必须显式启用。
    • 延迟调用android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。
    • 不要使用ILoggerFactory创建通用记录器。而是直接获取ILogger服务,这样它就被缓存了。

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

    使用工厂方法进行依赖注入

    当使用Microsoft.Extensions。DependencyInjection,注册服务,比如:

    1. IServiceCollection services /* ... */;
    2. services.TryAddSingleton<IFooService, FooService>();

    Microsoft.Extensions必须做一些System.Reflection来创建FooService的第一个实例。这是值得注意的dotnet跟踪输出在android上。

    相反,如果你这样做了:

    1. // If FooService has no dependencies
    2. services.TryAddSingleton<IFooService>(sp => new FooService());
    3. // Or if you need to retrieve some dependencies
    4. services.TryAddSingleton<IFooService>(sp => new FooService(sp.GetService<IBar>()));

    在这种情况下,Microsoft.Extensions可以简单地调用lamdba/匿名方法,而不需要系统。反射。

    我们在所有的dotnet/maui上进行了改进,并使用了bannedapianalyzer,这样就不会有人意外地使用TryAddSingleton()更慢的重载。

    请参阅dotnet/maui#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#6727xamarin-macios#14130了解关于这个改进的详细信息。

    懒惰地负载ConfigurationManager

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

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

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

    改进内置AOT配置文件

    Mono运行时有一个关于每个方法的JIT时间的报告(参见我们的文档),例如:

    1. Total(ms) | Self(ms) | Method
    2. 3.51 | 3.51 | Microsoft.Maui.Layouts.GridLayoutManager/GridStructure:.ctor (Microsoft.Maui.IGridLayout,double,double)
    3. 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 ()
    4. 1.66 | 1.66 | Microsoft.Maui.Controls.Xaml.OnIdiomExtension/<>c__DisplayClass32_0:<ProvideValue>g__minforetriever|0 ()
    5. 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

    1. _ = new Microsoft.Maui.Layouts.GridLayoutManager(new Grid()).Measure(100, 100);
    2. <SolidColorBrush x:Key="ProfiledAot_AppThemeBinding_Color" Color="{AppThemeBinding Default=Black}"/>
    3. <CollectionView x:Key="ProfiledAot_CollectionView_OnIdiom_Thickness" Margin="{OnIdiom Default=1,1,1,1}" />

    在这个测试应用程序中调用这些方法可以确保它们位于内置的. net MAUI AOT配置文件中。

    在这个更改之后,我们看了一个更新的JIT报告:

    1. Total (ms) | Self (ms) | Method
    2. 2.61 | 2.61 | string:SplitInternal (string,string[],int,System.StringSplitOptions)
    3. 1.57 | 1.57 | System.Number:NumberToString (System.Text.ValueStringBuilder&,System.Number/NumberBuffer&,char,int,System.Globalization.NumberFormatInfo)
    4. 1.52 | 1.52 | System.Number:TryParseInt32IntegerStyle (System.ReadOnlySpan`1<char>,System.Globalization.NumberStyles,System.Globalization.NumberFormatInfo,int&)

    这导致了进一步的补充:

    1. var split = "foo;bar".Split(';');
    2. var x = int.Parse("999");
    3. 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:

    1. namespace System
    2. {
    3. internal static class UriHelper
    4. {
    5. internal static readonly Encoding s_noFallbackCharUTF8 = Encoding.GetEncoding(
    6. 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 ctorpublic 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次:

    1. Before
    2. Average(ms): 843.7
    3. Average(ms): 847.8
    4. After

    对于这种类型的更改,总是建议根据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这样的文件时,你会得到一个像这样的字段:

    1. package com.yourlibrary;
    2. public class R
    3. {
    4. public class drawable
    5. {
    6. // The actual integer here maps to a table inside the final .apk file
    7. public final int foo = 1234;
    8. }
    9. }

    你可以使用这个值,例如,在ImageView中显示这个图像:

    1. ImageView imageView = new ImageView(this);
    2. imageView.setImageResource(R.drawable.foo);

    当你构建com.yourlibrary.aar时, android的gradle插件实际上并没有把这个类放在包中。相反,android应用程序实际上知道整数的值是多少。因此,R类是在android应用程序构建时生成的,为每个android库生成一个R类。

    Xamarin.Android采取了不同的方法,在运行时进行整数修复。用c#和MSBuild做这样的事情真的没有一个很好的先例吗?例如,一个c# android库可能有:

    1. public class Resource
    2. {
    3. public class Drawable
    4. {
    5. // The actual integer here is *not* final
    6. public int foo = -1;
    7. }
    8. }

    然后主应用程序就会有如下代码:

    1. public class Resource
    2. {
    3. public class Drawable
    4. {
    5. public Drawable()
    6. {
    7. // Copy the value at runtime
    8. global::MyLibrary.Resource.Drawable.foo = foo;
    9. }
    10. // The actual integer here *is* final
    11. public const int foo = 1234;
    12. }
    13. }

    这种情况已经很好地运行了一段时间,但不幸的是,像androidX、Material、谷歌Play Services等谷歌的库中的资源数量已经开始复合。例如,在dotnet/maui#2606中,启动时设置了21497个字段!我们创建了一种方法来解决这个问题,但我们也有一个新的自定义修剪步骤来执行修复在构建时(在修剪期间)而不是在运行时。

    <AndroidLinkResources>true</ AndroidLinkResources>

    这将使你的版本版本替换案例如下:

    1. ImageView imageView = new(this);
    2. imageView.SetImageResource(Resource.Drawable.foo);

    相反,直接内联整数:

    1. ImageView imageView = new(this);
    2. imageView.SetImageResource(1234); // The actual integer here *is* final

    这个特性的一个已知问题是:

    1. public partial class Styleable
    2. {
    3.     public static int[] ActionBarLayout = new int[] { 16842931 };
    4. }

    目前不支持替换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中添加以下内容:

    1. <!-- NOTE: not recommended for Debug builds! -->
    2. <AndroidLinkTool Condition="'$(Configuration)' == 'Release'">r8</AndroidLinkTool>

    如果启动你的应用程序的Release构建在启用后崩溃,检查adb logcat输出,看看哪里出了问题。

    如果你看到java.lang. classnotfoundexception或java.lang。你可能需要添加一个ProguardConfiguration文件到你的项目中,比如:

    1. <ItemGroup>
    2.   <ProguardConfiguration Include="proguard.cfg" />
    3. </ItemGroup>
    4. -keep class com.thepackage.TheClassYouWantToPreserve { *; <init>(...); }

    我们正在研究在未来的.NET版本中默认启用R8的选项。

    详情请参阅我们的D8/R8文档

    AOT

    Profiled AOT是默认的,因为它在应用程序大小和启动性能之间给出了最好的权衡。如果应用程序的大小与你的应用程序无关,你可以考虑对所有.NET程序集使用AOT。

    要选择加入,在你的.csproj中添加以下Release配置:

    1. <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    2.   <RunAOTCompilation>true</RunAOTCompilation>
    3.   <androidEnableProfiledAot>false</androidEnableProfiledAot>
    4. </PropertyGroup>

    这将减少在应用程序启动期间发生的JIT编译量,以及导航到后面的屏幕等。

    AOTLLVM

    LLVM提供了一个独立于源和目标的现代优化器,可以与Mono AOT Compiler输出相结合。其结果是,应用的尺寸略大,发行构建时间更长,运行时性能更好。   

    要选择将LLVM用于Release版本,请将以下内容添加到你的.csproj中:

    1. <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    2.   <RunAOTCompilation>true</RunAOTCompilation>
    3.   <EnableLLVM>true</EnableLLVM>
    4. </PropertyGroup>

    此特性可以与Profiled AOT(或AOT-ing一切)结合使用。对比应用程序的前后,了解EnableLLVM对应用程序大小和启动性能的影响。

    目前,需要安装一个android NDK来使用这个功能。如果我们能够解决这个需求,EnableLLVM将成为未来.NET版本中的默认选项。

    有关详细信息,请参阅我们关于EnableLLVM的文档

    记录自定义AOT配置文件

    概要AOT默认使用我们在.NET MAUI和android工作负载中提供的“内置”概要文件,对大多数应用程序都很有用。为了获得最佳的启动性能,理想情况下应该记录应用程序特定的配置文件。针对这种情况,我们有一个实验性的Mono.Profiler.Android包。

    记录配置文件:

    1. dotnet add package Mono.AotProfiler.android
    2. dotnet build -t:BuildAndStartAotProfiling
    3. # Wait until app launches, or you navigate to a screen
    4. dotnet build -t:FinishAotProfiling

    这将在你的项目目录下产生一个custom.aprof。要在未来的构建中使用它:

    1. <ItemGroup>
    2.   <androidAotProfile Include="custom.aprof" />
    3. </ItemGroup>

    我们正在努力在未来的.NET版本中完全支持记录自定义概要文件。

    希望您喜欢我们的.NET MAUI性能论述。请尝试.NET MAUI并且可以在.NET Multi-platform App UI (.NET MAUI) | .NET了解更多!


    关注微软中国MSDN公众号了解更多                       

    点击获取更多学习资源~

  • 相关阅读:
    Linux 系统与本地 windows 系统相差30s左右问题解决方案
    基于springboot的教材预定系统平台
    Linux c编程之TCP通信
    如何将 Bootstrap CSS 和 JS 添加到 Thymeleaf
    ZigBee 3.0理论教程-通用-1-10:安全加密-应用子层(APS)安全
    C++11 新特性
    【测试人生】UE4大世界游戏寻路效果自动化测试
    单例模式使用饿汉式和懒汉式创建一定安全?很多人不知
    【Vite + Vue3】ElementPlus el-select 实现下拉选择图标,并将图标回显到选择框中
    Hbase学习日记:四、Hbase架构
  • 原文地址:https://blog.csdn.net/helendemeng/article/details/125453159