.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。
内容十分丰富,来看是否有您期待的更新吧!
主要内容- 启动性能的改进
- 在移动设备上进行分析
- 测量随着时间的推移
- 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中未使用的编码对象
应用程序大小的改进
- 修复默认的MauiImage大小
- 删除Application.Properties 和DataContractSerializer:
- 修剪未使用的HTTP实现
.NET Podcast示例中的改进
- 删除Microsoft.Extensions.Http用法
- 删除Newtonsoft.Json使用
- 在后台运行第一个网络请求
实验性或高级选项
- 修剪Resource.designer.cs
- R8 Java代码收缩器
- AOT一切
- AOT和LLVM
- 记录自定义AOT配置文件
我必须提到移动平台上可用的.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输出时,你会注意到连接:
Pressor to exit...812 (KB)
在您的应用程序完全启动后,只需按下enter键就可以得到一个保存在当前目录的*.speedscope。你可以在https://speedscope.app上打开这个文件,深入了解每个方法在应用程序启动期间所花费的时间:
在android应用程序中使用dotnet跟踪的更多细节,请参阅我们的文档。我建议在android设备上分析Release版本,以获得应用在现实世界中的最佳表现。
测量随着时间的推移我们在.NET基础团队的朋友建立了一个管道来跟踪.NET MAUI性能场景,例如:
- 包大小
- 磁盘大小(未压缩)
- 单个文件分类
- 应用程序启动
随着时间的推移,这使我们能够看到改进或回归的影响,看到dotnet/maui回购的每个提交的数字。我们还可以确定这种差异是否是由xamarin-android、xamarin-macios或dotnet/runtime中的变化引起的。
例如,在物理Pixel 4a设备上运行的dotnet new maui模板的启动时间(以毫秒为单位)图:
注意,Pixel 4a比Pixel 5要慢得多。
我们可以精确地指出在dotnet/maui中发生的回归和改进。这对于追踪我们的目标是非常有用的。
同样地,我们可以在相同的Pixel 4a设备上看到.NET Podcast应用随着时间的推移所取得的进展:
这张图表是我们真正关注的焦点,因为它是一款“真正的应用”,接近于开发者在自己的手机应用中看到的内容。
至于应用程序大小,它是一个更稳定的数字——当情况变得更糟或更好时,它很容易归零:
请参阅dotnet-podcasts#58, Android x# 520和dotnet/maui#6419了解这些改进的详细信息。
异形AOT在我们对.NET MAUI的初始性能测试中,我们看到了JIT(及时)和AOT(提前)编译的代码是如何执行的:
应用
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了解这个改进的细节。
单文件程序集存储器之前,如果你在你最喜欢的zip文件实用程序中查看Release android .apk内容,你可以看到.NET程序集位于:
assemblies/Java.Interop.dll assemblies/Mono.android.dll assemblies/System.Runtime.dll assemblies/arm64-v8a/System.Private.CoreLib.dll assemblies/armeabi-v7a/System.Private.CoreLib.dll assemblies/x86/System.Private.CoreLib.dll assemblies/x86_64/System.Private.CoreLib.dll
这些文件是通过mmap系统调用单独加载的,这是应用程序中每个.NET程序集的成本。这是在android工作负载中用C/ c++实现的,使用Mono运行时为程序集加载提供的回调。MAUI应用程序有很多程序集,所以我们引入了一个新的$(androidUseAssemblyStore)特性,该特性在Release版本中默认启用。
在这个改变之后,你会得到:
assemblies/assemblies.manifest assemblies/assemblies.blob assemblies/assemblies.arm64_v8a.blob assemblies/assemblies.armeabi_v7a.blob assemblies/assemblies.x86.blob assemblies/assemblies.x86_64.blob
现在android启动只需要调用mmap两次:一次是assemblies.blob,第二次是特定于体系结构的Blob。这对带有许多. net程序集的应用程序产生了明显的影响。
如果你需要检查编译过的android应用程序中这些程序集的IL,我们创建了一个程序集存储读取器工具来“解包”这些文件。
另一个选择是在构建应用程序时禁用这些设置:
dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:Android EnableAssemblyCompression=false
这样你就可以用你喜欢的压缩工具解压生成的.apk文件,并使用ILSpy这样的工具来检查.NET程序集。这是一个很好的方法来诊断修剪器/链接器问题。
查看xamarin-android#6311了解关于这个改进的详细信息。
Spanify RegisterNativeMembers当用Java创建c#对象时,会调用一个小型的Java包装器,例如:
public class MainActivity extends Android.app.Activity { public static final String methods; static { methods = "n_onCreate:(LAndroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n"; mono.Android.Runtime.register ("foo.MainActivity, foo", MainActivity.class, methods); }
方法列表是一个以\n和:分隔的Java本机接口(JNI)签名列表,这些签名在托管的c#代码中被重写。对于在c#中重写的每个Java方法,您都会得到一个这样的方法。
当实际的Java onCreate()方法被调用为一个android活动:
public void onCreate (Android.os.Bundle p0) { n_onCreate (p0); } private native void n_onCreate (Android.os.Bundle p0);
通过各种各样的魔术和手势,n_onCreate调用到Mono运行时,并调用c#中的OnCreate()方法。
拆分\n和:-分隔的方法列表的代码是在Xamarin早期使用string.Split()编写的。可以说,Span在那时还不存在,但我们现在可以使用它!这提高了任何继承Java类的c#类的成本,因此这是一个比.NET MAUI更广泛的改进。
你可能会问,“为什么要使用字符串呢?”使用Java数组似乎比分隔字符串对性能的影响更大。在我们的测试中,调用JNI来获取Java数组元素,性能比字符串差。Split和Span的新用法。对于如何在未来的.NET版本中重新构建它,我们有一些想法。
除了.NET 6之外,针对当前客户Xamarin. Android的最新版本也附带了这一更改。
查看xamarin-android#6708了解关于此改进的详细信息。
System.Reflection.Emit和构造函数在使用Xamarin的早期,我们有一个从Java调用c#构造函数的有点复杂的方法。
首先,我们有一些在启动时发生的反射调用:
static MethodInfo newobject = typeof (System.Runtime.CompilerServices.RuntimeHelpers).GetMethod ("GetUninitializedObject", BindingFlags.Public | BindingFlags.Static)!; static MethodInfo gettype = typeof (System.Type).GetMethod ("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static)!; static FieldInfo handle = typeof (Java.Lang.Object).GetField ("handle", BindingFlags.NonPublic | BindingFlags.Instance)!;
这似乎是Mono早期版本遗留下来的,并一直延续到今天。例如,可以直接调用RuntimeHelpers.GetUninitializedObject()。
然后是一些复杂的System.Reflection.Emit用法,并在System.Reflection.ConstructorInfo中传递一个cinfo实例:
DynamicMethod method = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), typeof (void), new Type [] {typeof (IntPtr), typeof (object []) }, typeof (DynamicMethodNameCounter), true); ILGenerator il = method.GetILGenerator (); il.DeclareLocal (typeof (object)); il.Emit (OpCodes.Ldtoken, type); il.Emit (OpCodes.Call, gettype); il.Emit (OpCodes.Call, newobject); il.Emit (OpCodes.Stloc_0); il.Emit (OpCodes.Ldloc_0); il.Emit (OpCodes.Ldarg_0); il.Emit (OpCodes.Stfld, handle); il.Emit (OpCodes.Ldloc_0); var len = cinfo.GetParameters ().Length; for (int i = 0; i < len; i++) { il.Emit (OpCodes.Ldarg, 1); il.Emit (OpCodes.Ldc_I4, i); il.Emit (OpCodes.Ldelem_Ref); } il.Emit (OpCodes.Call, cinfo); il.Emit (OpCodes.Ret); return (Action) method.CreateDelegate (typeof (Action ));
调用返回的委托,使得IntPtr是Java.Lang.Object子类的句柄,而对象[]是该特定c#构造函数的任何参数。emit对于在启动时第一次使用它以及以后的每次调用都有很大的成本。
经过仔细的审查,我们可以将handle字段设置为内部的,并将此代码简化为:
var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType); if (newobj is Java.Lang.Object o) { o.handle = jobject; } else if (newobj is Java.Lang.Throwable throwable) { throwable.handle = jobject; } else { throw new InvalidOperationException ($"Unsupported type: '{newobj}'"); } cinfo.Invoke (newobj, parms);
这段代码所做的是在不调用构造函数的情况下创建一个对象,设置句柄字段,然后调用构造函数。这样做是为了当c#构造函数开始时,Handle在任何Java.Lang.Object上都是有效的。构造函数内部的任何Java互操作(比如调用类上的其他Java方法)以及调用任何基本Java构造函数都需要Handle。
新代码显著改进了从Java调用的任何c#构造函数,因此这个特殊的更改改进的不仅仅是.NET MAUI。除了.NET 6之外,针对当前客户Xamarin. android的最新版本也附带了这一更改。
查看xamarin-android#6766了解这个改进的详细信息。
System.Reflection.Emit和方法当你在c#中重写一个Java方法时,比如:
public class MainActivity : Activity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); //... } }
在从Java到c#的转换过程中,我们必须封装c#方法来处理异常,例如:
try { // Call the actual C# method here } catch (Exception e) when (_unhandled_exception (e)) { androidEnvironment.UnhandledException (e); if (Debugger.IsAttached || !JNIEnv.PropagateExceptions) throw; }
例如,如果在OnCreate()中未处理托管异常,那么实际上会导致本机崩溃(并且没有托管的c#堆栈跟踪)。我们需要确保调试器在附加异常时能够中断,否则将记录c#堆栈跟踪。
从Xamarin开始,上面的代码是通过System.Reflection.Emit生成的:
var dynamic = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), ret_type, param_types, typeof (DynamicMethodNameCounter), true); var ig = dynamic.GetILGenerator (); LocalBuilder? retval = null; if (ret_type != typeof (void)) retval = ig.DeclareLocal (ret_type); ig.Emit (OpCodes.Call, wait_for_bridge_processing_method!); var label = ig.BeginExceptionBlock (); for (int i = 0; i < param_types.Length; i++) ig.Emit (OpCodes.Ldarg, i); ig.Emit (OpCodes.Call, dlg.Method); if (retval != null) ig.Emit (OpCodes.Stloc, retval); ig.Emit (OpCodes.Leave, label); bool filter = Debugger.IsAttached || !JNIEnv.PropagateExceptions; if (filter && JNIEnv.mono_unhandled_exception_method != null) { ig.BeginExceptFilterBlock (); ig.Emit (OpCodes.Call, JNIEnv.mono_unhandled_exception_method); ig.Emit (OpCodes.Ldc_I4_1); ig.BeginCatchBlock (null!); } else { ig.BeginCatchBlock (typeof (Exception)); } ig.Emit (OpCodes.Dup); ig.Emit (OpCodes.Call, exception_handler_method!); if (filter) ig.Emit (OpCodes.Throw); ig.EndExceptionBlock (); if (retval != null) ig.Emit (OpCodes.Ldloc, retval); ig.Emit (OpCodes.Ret);
这段代码被调用两次为一个 dotnet new android 应用程序,但~58次为一个dotnet new maui应用程序!
我们意识到实际上可以为每个通用委托类型编写一个强类型的“快速路径”,而不是使用System.Reflection.Emit。有一个生成的委托匹配每个签名:
void OnCreate(Bundle savedInstanceState); // Maps to *JNIEnv, JavaClass, Bundle // Internal to each assembly internal delegate void _JniMarshal_PPL_V(IntPtr, IntPtr, IntPtr);
这样我们就可以列出所有使用过的dotnet maui应用程序的签名,比如:
class JNINativeWrapper { static Delegate? CreateBuiltInDelegate (Delegate dlg, Type delegateType) { switch (delegateType.Name) { // Unsafe.As() is used, because _JniMarshal_PPL_V is generated internal in each assembly case nameof (_JniMarshal_PPL_V): return new _JniMarshal_PPL_V (Unsafe.As(dlg).Wrap_JniMarshal_PPL_V); // etc. } return null; } // Static extension method is generated to avoid capturing variables in anonymous methods internal static void Wrap_JniMarshal_PPL_V (this _JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0) { // ... } }
这种方法的缺点是,当使用新签名时,我们必须列出更多的情况。不想详尽地列出每一种组合,因为这会导致IL大小的增长。我们正在研究如何在未来的.NET版本中改进这一点。
查看xamarin-android#6657和xamarin- android #6707了解这个改进的详细信息。
更新的Java.Interop APIsJava.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来回传递c#数组时,中间步骤必须复制数组,以便适当的运行时能够访问它。这真的是一个开发者体验的情况,因为c#开发者期望写这样的东西:
var array = new int[] { 1, 2, 3, 4}; MyJavaMethod (array); 在MyJavaMethod里面会做: IntPtr native_items = JNIEnv.NewArray (items); try { // p/invoke here, actually calls into Java } finally { if (items != null) { JNIEnv.CopyArray (native_items, items); // If the calling method mutates the array JNIEnv.DeleteLocalRef (native_items); // Delete our Java local reference } }
JNIEnv.NewArray()访问一个“类型映射”,以知道需要将哪个Java类用于数组的元素。
dotnet new maui项目使用的特定android API有问题:
public ColorStateList (int[][]? states, int[]? colors)
发现一个多维 int[][] 数组可以访问每个元素的“类型映射”。 当启用额外的日志记录时,我们可以看到这一点,许多实例:
monodroid: typemap: failed to map managed type to Java type: System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e (Module ID: 8e4cd939-3275-41c4-968d-d5a4376b35f5; Type token: 33554653) monodroid-assembly: typemap: called from monodroid-assembly: at android.Runtime.JNIEnv.TypemapManagedToJava(Type ) monodroid-assembly: at android.Runtime.JNIEnv.GetJniName(Type ) monodroid-assembly: at android.Runtime.JNIEnv.FindClass(Type ) monodroid-assembly: at android.Runtime.JNIEnv.NewArray(Array , Type ) monodroid-assembly: at android.Runtime.JNIEnv.NewArray[Int32[]](Int32[][] ) monodroid-assembly: at android.Content.Res.ColorStateList..ctor(Int32[][] , Int32[] ) monodroid-assembly: at Microsoft.Maui.Platform.ColorStateListExtensions.CreateButton(Int32 enabled, Int32 disabled, Int32 off, Int32 pressed)
对于这种情况,我们应该能够调用JNIEnv.FindClass()一次,并为数组中的每一项重用这个值!
我们正在研究如何在未来的.NET版本中进一步改进这一点。一个这样的例子是dotnet/maui#5654,在这里我们只是简单地考虑完全用Java来创建数组。
查看xamarin-android#6870了解这个改进的详细信息。
为android图像使用GlideGlide是现代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) { //... RequestBuilderbuilder = Glide .with(imageView) .load(androidUri); loadInto(builder, imageView, cachingEnabled, callback); }
ImageLoaderCallback在c#中子类化以处理托管代码中的完成。其结果是,来自web的图像的性能应该比以前在Xamarin.Forms中得到的性能有了显著提高。
详见dotnet/maui#759和dotnet/maui#5198。
减少Java互操作调用假设你有以下Java api:
public void setFoo(int foo); public void setBar(int bar);
这些方法的互操作如下:
public unsafe static void SetFoo(int foo) { JniArgumentValue* __args = stackalloc JniArgumentValue[1]; __args[0] = new JniArgumentValue(foo); return _members.StaticMethods.InvokeInt32Method("setFoo.(I)V", __args); } public unsafe static void SetBar(int bar) { JniArgumentValue* __args = stackalloc JniArgumentValue[1]; __args[0] = new JniArgumentValue(bar); return _members.StaticMethods.InvokeInt32Method("setBar.(I)V", __args);
所以调用这两个方法会两次调用stackalloc,两次调用p/invoke。创建一个小型的Java包装器会更有性能,例如:
public void setFooAndBar(int foo, int bar) { setFoo(foo); setBar(bar); }
翻译为:
public unsafe static void SetFooAndBar(int foo, int bar) { JniArgumentValue* __args = stackalloc JniArgumentValue[2]; __args[0] = new JniArgumentValue(foo); __args[1] = new JniArgumentValue(bar); return _members.StaticMethods.InvokeInt32Method("setFooAndBar.(II)V", __args); }
.NET MAUI视图本质上是c#对象,有很多属性需要在Java中以完全相同的方式设置。如果我们将这个概念应用到.NET MAUI中的每个android View中,我们可以创建一个~18参数的方法用于View创建。后续的属性更改可以直接调用标准的android api。
对于非常简单的.NET MAUI控件来说,这在性能上有了显著的提高:
方法
平均
错误
标准差
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中看到了一个更简单的布局,底部标签导航:
我们可以将其移植到四个Java方法中,例如:
@NonNull public static ListcreateBottomTabLayout(Context context, int navigationStyle); @NonNull public static LinearLayout createLinearLayout(Context context); @NonNull public static FrameLayout createFrameLayout(Context context, LinearLayout layout); @NonNull public static BottomNavigationView createNavigationBar(Context context, int navigationStyle, FrameLayout bottom)
这使得我们在android上创建底部标签导航时只能从c#切换到Java 4次。它还允许android操作系统跳过加载和解析.xml来“膨胀”Java对象。我们在dotnet/maui中执行了这个想法,在启动时删除所有LayoutInflater.Inflate()调用。
请参阅dotnet/maui#5424, dotnet/maui#5493,和dotnet/maui#5528了解这些改进的详细信息。删除Microsoft.Extensions.Hosting
hosting提供了一个.NET通用主机,用于在.NET应用程序中管理依赖注入、日志记录、配置和应用生命周期。这对启动时间有影响,似乎不适合移动应用程序。
从.NET MAUI中移除Microsoft.Extensions.Hosting使用是有意义的。. net MAUI没有试图与“通用主机”互操作来构建DI容器,而是有自己的简单实现,它针对移动启动进行了优化。此外,. net MAUI默认不再添加日志记录提供程序。
通过这一改变,我们看到dotnet new maui android应用程序的启动时间减少了5-10%。在iOS上,它减少了相同应用程序的大小,从19.2 MB => 18.0 MB。
详见dotnet/maui#4505和dotnet/maui#4545。
在启动时减少Shell初始化Xamarin. Forms Shell是跨平台应用程序导航的一种模式。这个模式是在.NET MAUI中提出的,它被推荐作为构建应用程序的默认方式。
当我们发现在启动时使用Shell的成本(对于Xamarin和Xamarin.form和.NET MAUI),我们找到了几个可以优化的地方:
- 不要在启动时解析路由——要等到一个需要它们的导航发生。
- 如果没有为导航提供查询字符串,则只需跳过处理查询字符串的代码。这将删除过度使用System.Reflection的代码路径。
- 如果页面没有可见的BottomNavigationView,那么不要设置菜单项或任何外观元素。
请参阅dotnet/maui#5262了解此改进的详细信息。
字体不应该使用临时文件大量的时间花在.NET MAUI应用程序加载字体上:
32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3)
检查代码时,它所做的工作比需要的更多:
- 将androidAsset文件保存到临时文件夹。
- 使用android API, Typeface.CreateFromFile()来加载文件。
我们实际上可以直接使用Typeface.CreateFromAsset() android API,根本不用临时文件。
请参阅dotnet/maui#4933了解有关此改进的详细信息。
编译时在平台上计算{OnPlatform}标记扩展的使用:
…实际上可以在编译时计算,net6.0-android和net6.0-ios会得到适当的值。在未来的.NET版本中,我们将对 XML元素进行同样的优化。
详见dotnet/maui#4829和dotnet/maui#5611。
在XAML中使用编译转换器以下类型现在在XAML编译时转换,而不是在运行时:
- 颜色:dotnet /maui# 4687
- 角半径: dotnet / maui # 5192
- 字形大小:dotnet / maui # 5338
- 网格长度, 行定义, 列定义: dotnet/maui#5489
这导致从.xaml文件生成更好/更快的IL。
优化颜色解析Microsoft.Maui.Graphics.Color.Parse()的原始代码可以重写,以更好地使用Span并避免字符串分配。
方法
平均
错误
标准差
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
能够在ReadonlySpandotnet/csharplang#1881上使用switch语句,将在未来的.NET版本中进一步改善这种情况。
看到dotnet / Microsoft.Maui.Graphics # 343和dotnet / Microsoft.Maui.Graphics # 345关于这个改进的细节。
不要使用区域性识别的字符串比较回顾一个新的naui项目的dotnet跟踪输出,可以看到android上第一个区域性感知字符串比较的真实成本:
6.32ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellNavigationManager.GetNavigationState 3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri 3.82ms System.Private.CoreLib!System.String.StartsWith 2.57ms System.Private.CoreLib!System.Globalization.CultureInfo.get_CurrentCulture
实际上,我们甚至不希望在本例中使用区域性比较—它只是从Xamarin.Forms引入的代码。
例如,如果你有:
if (text.StartsWith("f")) { // do something }
在这种情况下,你可以简单地这样做:
if (text.StartsWith("f", StringComparision.Ordinal)) { // do something }
如果在整个应用程序中执行,System.Globalization.CultureInfo.CurrentCulture可以避免被调用,并且可以稍微提高If语句的总体速度。
为了解决整个dotnet/maui回购的这种情况,我们引入了代码分析规则来捕捉这些:
dotnet_diagnostic.CA1307.severity = error dotnet_diagnostic.CA1309.severity = error
请参阅dotnet/maui#4988了解有关改进的详细信息。
懒惰地创建日志ConfigureFonts() API在启动时花费了一些时间来做一些可以延迟到以后的工作。我们还可以改进Microsoft.Extensions中日志基础设施的一般用法。
我们所做的一些改进如下:
- 推迟创建“记录器”类,直到需要它们时再创建。
- 内置的日志记录基础设施在默认情况下是禁用的,必须显式启用。
- 延迟调用android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。
- 不要使用ILoggerFactory创建通用记录器。而是直接获取ILogger服务,这样它就被缓存了。
请参阅dotnet/maui#5103了解有关此改进的详细信息。
使用工厂方法进行依赖注入当使用Microsoft.Extensions。DependencyInjection,注册服务,比如:
IServiceCollection services /* ... */; services.TryAddSingleton();
Microsoft.Extensions必须做一些System.Reflection来创建FooService的第一个实例。这是值得注意的dotnet跟踪输出在android上。
相反,如果你这样做了:
// If FooService has no dependencies services.TryAddSingleton(sp => new FooService()); // Or if you need to retrieve some dependencies services.TryAddSingleton(sp => new FooService(sp.GetService()));
在这种情况下,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#6727和xamarin-macios#14130了解关于这个改进的详细信息。
懒惰地负载ConfigurationManagerconfigurationmanager并没有被许多移动应用程序使用,而且创建一个是非常昂贵的!(例如,在android上约为7.59ms)
在.NET MAUI中,一个ConfigurationManager在启动时默认被创建,我们可以使用Lazy延迟它的创建,所以它将不会被创建,除非请求。
请参阅dotnet/maui#5348了解有关此改进的详细信息。
改进内置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:关注打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?