当前位置:网站首页>.NET MAUI 性能提升
.NET MAUI 性能提升
2022-07-07 09:35:00 【dotNET跨平台】
点击蓝字
关注我们
作者:Jonathan Peppers
翻译:Yijing Sun
校稿:Amy Peng
排版:Rani Sun
精彩预告
*本文干货满满,预计阅读时间32分钟,建议收藏保存。
.NET多平台应用程序UI (MAUI)将android、iOS、macOS和Windows API统一为一个API,这样你就可以编写一个应用程序在许多平台上本机运行。我们专注于提高您的日常生产力以及您的应用程序的性能。我们认为,开发人员生产率的提高不应该以应用程序性能为代价。
应用程序的大小也是如此——在一个空白的.NET MAUI应用程序中存在什么开销?当我们开始优化.NET MAUI时,很明显iOS需要做一些工作来改善应用程序的大小,而android则缺乏启动性能。
一个dotnet new maui项目的iOS应用程序最初大约是18MB。同样,在之前的预览中.NET MAUI在android上的启动时间也不是很理想:
应用程序 | 框架 | 启动时间(ms) |
Xamarin.Android | Xamarin | 306.5 |
Xamarin.Forms | Xamarin | 498.6 |
Xamarin.Forms (Shell) | Xamarin | 817.7 |
dotnet new android | .NET 6 (早期预览) | 210.5 |
dotnet new maui | .NET 6 (早期预览) | 683.9 |
.NET Podcast | .NET 6 (早期预览) | 1299.9 |
这是在Pixel 5设备上平均运行10次得到的结果。有关这些数字是如何获得的,请参阅我们的maui-profiling文件。
我们的目标是让.NET MAUI比它的前身Xamarin更快。很明显,我们在.NET MAUI本身也有一些工作要做。dotnet new android 模板的发布速度已经超过Xamarin.Android,主要是因为.NET 6中新的BCL和Mono运行时。
新的.NET maui模板还没有使用Shell导航模式,但是计划将其作为.NET maui的默认导航模式。当我们采用这个更改时,我们知道会对模板中的性能造成影响。
几个不同团队的合作才有了今天的成就。我们改进了Microsoft.Extensions ,依赖注入的使用,AOT编译,Java互操作,XAML,.NET MAUI代码,等等方面。
尘埃落定后,我们达到了一个更好的阶段:
应用程序 | 框架 | 启动时间(ms) |
Xamarin.Android | Xamarin | 306.5 |
Xamarin.Forms | Xamarin | 498.6 |
Xamarin.Forms (Shell) | Xamarin | 817.7 |
dotnet new android | .NET 6 (MAUI GA) | 182.8 |
dotnet new maui (No Shell**) | .NET 6 (MAUI GA) | 464.2 |
dotnet new maui (Shell) | .NET 6 (MAUI GA) | 568.1 |
.NET Podcast App (Shell) | .NET 6 (MAUI GA) | 814.2 |
** -这是原始的dotnet new maui模板,没有使用Shell。
下面的细节,享受吧!
.NET Podcast
https://github.com/microsoft/dotnet-podcasts
maui-profiling
https://github.com/jonathanpeppers/maui-profiling
Shell
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/
.NET Podcast App (Shell)
https://github.com/microsoft/dotnet-podcasts
主要内容
启动性能的改进
在移动设备上进行分析
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiling-on-mobile
测量随着时间的推移
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#measuring-over-time
Profiled AOT
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#profiled-aot
单文件程序集存储器
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores
Spanify.RegisterNativeMembers
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#spanify-registernativemembers
System.Reflection.Emit和构造函数
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-constructors
System.Reflection.Emit和方法
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#systemreflectionemit-and-methods
更新的Java.Interop APIs
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays
多维Java数组
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays
为android图像使用Glide
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#multi-dimensional-java-arrays
减少Java互操作调用
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#reduce-java-interop-calls
将android XML移植到Java
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#port-android-xml-to-java
删除Microsoft.Extensions.Hosting
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshosting
在启动时减少Shell初始化
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#less-shell-initialization-on-startup
字体不应该使用临时文件
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fonts-should-not-use-temporary-files
编译时在平台上计算
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#compute-onplatform-at-compile-time
在XAML中使用编译转换器
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-compiled-converters-in-xaml
优化颜色解析
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#optimize-color-parsing
不要使用区域性识别的字符串比较
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#dont-use-culture-aware-string-comparisons
懒惰地创建日志
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily
使用工厂方法进行依赖注入
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#use-factory-methods-for-dependency-injection
懒惰地负载ConfigurationManager
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#create-loggers-lazily
默认VerifyDependencyInjectionOpenGenericServiceTrimmability
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#default-verifydependencyinjectionopengenericservicetrimmability
改进内置AOT配置文件
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#improve-the-built-in-aot-profile
启用AOT图像的延迟加载
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#enable-lazy-loading-of-aot-images
删除System.Uri中未使用的编码对象
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-unused-encoding-object-in-systemuri
应用程序大小的改进
修复默认的MauiImage大小
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fix-defaults-for-mauiimage-sizes
删除Application.Properties 和DataContractSerializer
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-applicationproperties-and-datacontractserializer
修剪未使用的HTTP实现
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trim-unused-http-implementations
.NET Podcast 示例中的改进(https://github.com/microsoft/dotnet-podcasts)
删除Microsoft.Extensions.Http用法
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshttp-usage
删除Newtonsoft.Json使用
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-newtonsoftjson-usage
在后台运行第一个网络请求
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#run-first-network-request-in-background
实验性或高级选项
修剪Resource.designer.cs
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trimming-resourcedesignercs
R8 Java代码收缩器
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#r8-java-code-shrinker
AOT一切
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-everything
AOT和LLVM
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-and-llvm
记录自定义AOT配置文件
https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#record-a-custom-aot-profile
启动性能的改进
在移动设备上进行分析
我必须提到移动平台上可用的.NET诊断工具,因为它是我们使.NET MAUI更快的第0步。
分析.NET 6 android应用程序需要使用一个叫做 dotnet-dsrouter 的工具。该工具使dotnet跟踪连接到一个运行的移动应用程序在 android, iOS 等。这可能是我们用来分析 .NET MAUI 的最有影响力的工具。
要开始使用dotnet trace和dsrouter,首先通过adb配置一些设置并启动dsrouter:
adb reverse tcp:9000 tcp:9001
adb shell setprop debug.mono.profile '127.0.0.1:9000,suspend'
dotnet-dsrouter client-server -tcps 127.0.0.1:9001 -ipcc /tmp/maui-app --verbose debug
下一步启动dotnet跟踪,如:
dotnet-trace collect --diagnostic-port /tmp/maui-app --format speedscope
在启动一个使用-c Release和-p:androidEnableProfiler=true构建的android应用程序后,当dotnet trace输出时,你会注意到连接:
Press <Enter> or <Ctrl+C> to exit...812 (KB)
在您的应用程序完全启动后,只需按下enter键就可以得到一个保存在当前目录的*.speedscope。你可以在https://speedscope.app上打开这个文件,深入了解每个方法在应用程序启动期间所花费的时间:
在android应用程序中使用dotnet跟踪的更多细节,请参阅我们的文档。我建议在android设备上分析Release版本,以获得应用在现实世界中的最佳表现。
dotnet-dsrouter
https://docs.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dsrouter
我们的文档
https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/tracing.md
测量随着时间的推移
我们在.NET基础团队的朋友建立了一个管道来跟踪.NET MAUI性能场景,例如:
包大小
磁盘大小(未压缩)
单个文件分类
应用程序启动
随着时间的推移,这使我们能够看到改进或回归的影响,看到dotnet/maui回购的每个提交的数字。我们还可以确定这种差异是否是由xamarin-android、xamarin-macios或dotnet/runtime中的变化引起的。
例如,在物理Pixel 4a设备上运行的dotnet new maui模板的启动时间(以毫秒为单位)图:
注意,Pixel 4a比Pixel 5要慢得多。
我们可以精确地指出在dotnet/maui中发生的回归和改进。这对于追踪我们的目标是非常有用的。
同样地,我们可以在相同的Pixel 4a设备上看到.NET Podcast应用随着时间的推移所取得的进展:
这张图表是我们真正关注的焦点,因为它是一款“真正的应用”,接近于开发者在自己的手机应用中看到的内容。
至于应用程序大小,它是一个更稳定的数字——当情况变得更糟或更好时,它很容易归零:
请参阅dotnet-podcasts#58, Android x# 520和dotnet/maui#6419了解这些改进的详细信息。
dotnet-podcasts#58
https://github.com/microsoft/dotnet-podcasts
Android x# 520
https://github.com/xamarin/AndroidX/pull/520
dotnet/maui#6419
https://github.com/dotnet/maui/pull/6419
异形AOT
在我们对.NET MAUI的初始性能测试中,我们看到了JIT(及时)和AOT(提前)编译的代码是如何执行的:
应用 | JIT 时间(ms) | AOT 时间(ms) |
dotnet 新maui | 1078.0ms | 683.9ms |
每次调用c#方法时都会发生JIT处理,这会隐式地影响移动应用程序的启动性能。
另一个问题是AOT导致的应用程序大小增加。每个.NET程序集都会在最终应用中添加一个android本地库。为了更好地利用这两个世界,启动跟踪或分析AOT是Xamarin.Android当前的一个特性。这是一种AOT应用程序启动路径的机制,它显著提高了启动时间,而只增加了适度的应用程序大小。
在.NET 6版本中,这是完全有意义的默认选项。在过去,使用Xamarin.Android进行任何类型的AOT都需要Android NDK(下载多个gb)。我们在没有安装android NDK的情况下构建了AOT应用程序,使其成为可能。
我们为 dotnet new android, maui,和maui-blazor模板的内置配置文件,使大多数应用程序受益。如果你想在.NET 6中记录一个自定义配置文件,你可以试试我们的实验性的Mono.Profiler. Android包。我们正在努力在未来的.NET版本中完全支持记录自定义概要文件。
查看xamarin-Android#6547和dotnet/maui#4859了解这个改进的细节。
启动跟踪或分析AOT
https://devblogs.microsoft.com/xamarin/faster-startup-times-with-startup-tracing-on-android/
Mono.Profiler. Android
https://github.com/jonathanpeppers/Mono.Profiler.Android
xamarin-Android#6547
https://github.com/xamarin/xamarin-android/pull/6547
dotnet/maui#4859
https://github.com/dotnet/maui/pull/4859
单文件程序集存储器
之前,如果你在你最喜欢的zip文件实用程序中查看Release android .apk内容,你可以看到.NET程序集位于:
assemblies/Java.Interop.dll
assemblies/Mono.android.dll
assemblies/System.Runtime.dll
assemblies/arm64-v8a/System.Private.CoreLib.dll
assemblies/armeabi-v7a/System.Private.CoreLib.dll
assemblies/x86/System.Private.CoreLib.dll
assemblies/x86_64/System.Private.CoreLib.dll
这些文件是通过mmap系统调用单独加载的,这是应用程序中每个.NET程序集的成本。这是在android工作负载中用C/ c++实现的,使用Mono运行时为程序集加载提供的回调。MAUI应用程序有很多程序集,所以我们引入了一个新的$(androidUseAssemblyStore)特性,该特性在Release版本中默认启用。
在这个改变之后,你会得到:
assemblies/assemblies.manifest
assemblies/assemblies.blob
assemblies/assemblies.arm64_v8a.blob
assemblies/assemblies.armeabi_v7a.blob
assemblies/assemblies.x86.blob
assemblies/assemblies.x86_64.blob
现在android启动只需要调用mmap两次:一次是assemblies.blob,第二次是特定于体系结构的Blob。这对带有许多. net程序集的应用程序产生了明显的影响。
如果你需要检查编译过的android应用程序中这些程序集的IL,我们创建了一个程序集存储读取器工具来“解包”这些文件。
另一个选择是在构建应用程序时禁用这些设置:
dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:Android EnableAssemblyCompression=false
这样你就可以用你喜欢的压缩工具解压生成的.apk文件,并使用ILSpy这样的工具来检查.NET程序集。这是一个很好的方法来诊断修剪器/链接器问题。
查看xamarin-android#6311了解关于这个改进的详细信息。
mmap系统调用
https://man7.org/linux/man-pages/man2/mmap.2.html
mmap
https://man7.org/linux/man-pages/man2/mmap.2.html
程序集存储读取器
https://github.com/xamarin/xamarin-android/tree/main/tools/assembly-store-reader
ILSpy
https://github.com/icsharpcode/ILSpy
xamarin-android#6311
https://github.com/xamarin/xamarin-android/pull/6311
Spanify RegisterNativeMembers
当用Java创建c#对象时,会调用一个小型的Java包装器,例如:
public class MainActivity extends Android.app.Activity
{
public static final String methods;
static {
methods = "n_onCreate:(LAndroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n";
mono.Android.Runtime.register ("foo.MainActivity, foo", MainActivity.class, methods);
}
方法列表是一个以\n和:分隔的Java本机接口(JNI)签名列表,这些签名在托管的c#代码中被重写。对于在c#中重写的每个Java方法,您都会得到一个这样的方法。
当实际的Java onCreate()方法被调用为一个android活动:
public void onCreate (Android.os.Bundle p0)
{
n_onCreate (p0);
}
private native void n_onCreate (Android.os.Bundle p0);
通过各种各样的魔术和手势,n_onCreate调用到Mono运行时,并调用c#中的OnCreate()方法。
拆分\n和:-分隔的方法列表的代码是在Xamarin早期使用string.Split()编写的。可以说,Span在那时还不存在,但我们现在可以使用它!这提高了任何继承Java类的c#类的成本,因此这是一个比.NET MAUI更广泛的改进。
你可能会问,“为什么要使用字符串呢?”使用Java数组似乎比分隔字符串对性能的影响更大。在我们的测试中,调用JNI来获取Java数组元素,性能比字符串差。Split和Span的新用法。对于如何在未来的.NET版本中重新构建它,我们有一些想法。
除了.NET 6之外,针对当前客户Xamarin. Android的最新版本也附带了这一更改。
查看xamarin-android#6708了解关于此改进的详细信息。
Java本机接口(JNI)
https://en.wikipedia.org/wiki/Java_Native_Interface
Span
https://docs.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
xamarin-android#6708
https://github.com/xamarin/xamarin-android/pull/6708
System.Reflection.Emit和构造函数
在使用Xamarin的早期,我们有一个从Java调用c#构造函数的有点复杂的方法。
首先,我们有一些在启动时发生的反射调用:
static MethodInfo newobject = typeof (System.Runtime.CompilerServices.RuntimeHelpers).GetMethod ("GetUninitializedObject", BindingFlags.Public | BindingFlags.Static)!;
static MethodInfo gettype = typeof (System.Type).GetMethod ("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static)!;
static FieldInfo handle = typeof (Java.Lang.Object).GetField ("handle", BindingFlags.NonPublic | BindingFlags.Instance)!;
这似乎是Mono早期版本遗留下来的,并一直延续到今天。例如,可以直接调用RuntimeHelpers.GetUninitializedObject()。
然后是一些复杂的System.Reflection.Emit用法,并在System.Reflection.ConstructorInfo中传递一个cinfo实例:
DynamicMethod method = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), typeof (void), new Type [] {typeof (IntPtr), typeof (object []) }, typeof (DynamicMethodNameCounter), true);
ILGenerator il = method.GetILGenerator ();
il.DeclareLocal (typeof (object));
il.Emit (OpCodes.Ldtoken, type);
il.Emit (OpCodes.Call, gettype);
il.Emit (OpCodes.Call, newobject);
il.Emit (OpCodes.Stloc_0);
il.Emit (OpCodes.Ldloc_0);
il.Emit (OpCodes.Ldarg_0);
il.Emit (OpCodes.Stfld, handle);
il.Emit (OpCodes.Ldloc_0);
var len = cinfo.GetParameters ().Length;
for (int i = 0; i < len; i++) {
il.Emit (OpCodes.Ldarg, 1);
il.Emit (OpCodes.Ldc_I4, i);
il.Emit (OpCodes.Ldelem_Ref);
}
il.Emit (OpCodes.Call, cinfo);
il.Emit (OpCodes.Ret);
return (Action<IntPtr, object?[]?>) method.CreateDelegate (typeof (Action <IntPtr, object []>));
调用返回的委托,使得IntPtr是Java.Lang.Object子类的句柄,而对象[]是该特定c#构造函数的任何参数。emit对于在启动时第一次使用它以及以后的每次调用都有很大的成本。
经过仔细的审查,我们可以将handle字段设置为内部的,并将此代码简化为:
var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType);
if (newobj is Java.Lang.Object o) {
o.handle = jobject;
} else if (newobj is Java.Lang.Throwable throwable) {
throwable.handle = jobject;
} else {
throw new InvalidOperationException ($"Unsupported type: '{newobj}'");
}
cinfo.Invoke (newobj, parms);
这段代码所做的是在不调用构造函数的情况下创建一个对象,设置句柄字段,然后调用构造函数。这样做是为了当c#构造函数开始时,Handle在任何Java.Lang.Object上都是有效的。构造函数内部的任何Java互操作(比如调用类上的其他Java方法)以及调用任何基本Java构造函数都需要Handle。
新代码显著改进了从Java调用的任何c#构造函数,因此这个特殊的更改改进的不仅仅是.NET MAUI。除了.NET 6之外,针对当前客户Xamarin. android的最新版本也附带了这一更改。
查看xamarin-android#6766了解这个改进的详细信息。
xamarin-android#6766
https://github.com/xamarin/xamarin-android/pull/6766
System.Reflection.Emit和方法
当你在c#中重写一个Java方法时,比如:
public class MainActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
//...
}
}
在从Java到c#的转换过程中,我们必须封装c#方法来处理异常,例如:
try
{
// Call the actual C# method here
}
catch (Exception e) when (_unhandled_exception (e))
{
androidEnvironment.UnhandledException (e);
if (Debugger.IsAttached || !JNIEnv.PropagateExceptions)
throw;
}
例如,如果在OnCreate()中未处理托管异常,那么实际上会导致本机崩溃(并且没有托管的c#堆栈跟踪)。我们需要确保调试器在附加异常时能够中断,否则将记录c#堆栈跟踪。
从Xamarin开始,上面的代码是通过System.Reflection.Emit生成的:
var dynamic = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), ret_type, param_types, typeof (DynamicMethodNameCounter), true);
var ig = dynamic.GetILGenerator ();
LocalBuilder? retval = null;
if (ret_type != typeof (void))
retval = ig.DeclareLocal (ret_type);
ig.Emit (OpCodes.Call, wait_for_bridge_processing_method!);
var label = ig.BeginExceptionBlock ();
for (int i = 0; i < param_types.Length; i++)
ig.Emit (OpCodes.Ldarg, i);
ig.Emit (OpCodes.Call, dlg.Method);
if (retval != null)
ig.Emit (OpCodes.Stloc, retval);
ig.Emit (OpCodes.Leave, label);
bool filter = Debugger.IsAttached || !JNIEnv.PropagateExceptions;
if (filter && JNIEnv.mono_unhandled_exception_method != null) {
ig.BeginExceptFilterBlock ();
ig.Emit (OpCodes.Call, JNIEnv.mono_unhandled_exception_method);
ig.Emit (OpCodes.Ldc_I4_1);
ig.BeginCatchBlock (null!);
} else {
ig.BeginCatchBlock (typeof (Exception));
}
ig.Emit (OpCodes.Dup);
ig.Emit (OpCodes.Call, exception_handler_method!);
if (filter)
ig.Emit (OpCodes.Throw);
ig.EndExceptionBlock ();
if (retval != null)
ig.Emit (OpCodes.Ldloc, retval);
ig.Emit (OpCodes.Ret);
这段代码被调用两次为一个 dotnet new android 应用程序,但~58次为一个dotnet new maui应用程序!
我们意识到实际上可以为每个通用委托类型编写一个强类型的“快速路径”,而不是使用System.Reflection.Emit。有一个生成的委托匹配每个签名:
void OnCreate(Bundle savedInstanceState);
// Maps to *JNIEnv, JavaClass, Bundle
// Internal to each assembly
internal delegate void _JniMarshal_PPL_V(IntPtr, IntPtr, IntPtr);
这样我们就可以列出所有使用过的dotnet maui应用程序的签名,比如:
class JNINativeWrapper
{
static Delegate? CreateBuiltInDelegate (Delegate dlg, Type delegateType)
{
switch (delegateType.Name)
{
// Unsafe.As<T>() is used, because _JniMarshal_PPL_V is generated internal in each 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了解这个改进的详细信息。
xamarin-android#6657
https://github.com/xamarin/xamarin-android/pull/6657
xamarin- android #6707
https://github.com/xamarin/xamarin-android/pull/6707
更新的Java.Interop APIs
Java.Interop.dll中原始的Xamarin api是这样的api:
JNIEnv.CallStaticObjectMethod
在Java中调用的“新方法”每次调用占用的内存更少:
JniEnvironment.StaticMethods.CallStaticObjectMethod
当在构建时为Java方法生成c#绑定时,默认使用更新/更快的方法—在Xamarin.Android中已经有一段时间了。以前,Java绑定项目可以将$(AndroidCodegenTarget)设置为XAJavaInterop1,它在每次调用中缓存和重用jmethodID实例。请参阅java.interop文档获取关于该特性的历史记录。
其他有问题的地方是有“手动”绑定的地方。这些往往也是经常使用的方法,所以值得修复这些!
一些改善这种情况的例子:
JNIEnv.FindClass()在xamarin-android#6805
JavaList 和 JavaList在 xamarin-android#6812
AndroidCodegenTarget
https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#androidcodegentarget
java.interop
https://github.com/xamarin/Java.Interop/commit/d9b43b52a2904e00b74b96c82a7c62c6a0c214ca
xamarin-android#6805
https://github.com/xamarin/xamarin-android/pull/6805
xamarin-android#6812
https://github.com/xamarin/xamarin-android/pull/6812
多维Java数组
当向Java来回传递c#数组时,中间步骤必须复制数组,以便适当的运行时能够访问它。这真的是一个开发者体验的情况,因为c#开发者期望写这样的东西:
var array = new int[] { 1, 2, 3, 4};
MyJavaMethod (array);
在MyJavaMethod里面会做:
IntPtr native_items = JNIEnv.NewArray (items);
try
{
// p/invoke here, actually calls into Java
}
finally
{
if (items != null)
{
JNIEnv.CopyArray (native_items, items); // If the calling method mutates the 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了解这个改进的详细信息。
dotnet/maui#5654
https://github.com/dotnet/maui/pull/5654
xamarin-android#6870
https://github.com/xamarin/xamarin-android/pull/6870
为android图像使用Glide
Glide是现代android应用程序推荐的图片加载库。谷歌文档甚至推荐使用它,因为内置的android Bitmap类可能很难正确使用。glidex.forms是在Xamarin.Forms中使用Glide的原型。但我们将 Glide 提升为未来在 .NET MAUI 中加载图像的“方式”。
为了减少JNI互操作的开销,.NET MAUI的Glide实现主要是用Java编写的,例如:
import com.bumptech.glide.Glide;
//...
public static void loadImageFromUri(ImageView imageView, String uri, Boolean cachingEnabled, ImageLoaderCallback callback) {
//...
RequestBuilder<Drawable> builder = Glide
.with(imageView)
.load(androidUri);
loadInto(builder, imageView, cachingEnabled, callback);
}
ImageLoaderCallback在c#中子类化以处理托管代码中的完成。其结果是,来自web的图像的性能应该比以前在Xamarin.Forms中得到的性能有了显著提高。
详见dotnet/maui#759和dotnet/maui#5198。
Glide
https://github.com/bumptech/glide
glidex.forms
https://github.com/jonathanpeppers/glidex
dotnet/maui#759
https://github.com/dotnet/maui/pull/759
dotnet/maui#5198
https://github.com/dotnet/maui/pull/5198
减少Java互操作调用
假设你有以下Java api:
public void setFoo(int foo);
public void setBar(int bar);
这些方法的互操作如下:
public unsafe static void SetFoo(int foo)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
__args[0] = new JniArgumentValue(foo);
return _members.StaticMethods.InvokeInt32Method("setFoo.(I)V", __args);
}
public unsafe static void SetBar(int bar)
{
JniArgumentValue* __args = stackalloc JniArgumentValue[1];
__args[0] = new JniArgumentValue(bar);
return _members.StaticMethods.InvokeInt32Method("setBar.(I)V", __args);
}
所以调用这两个方法会两次调用stackalloc,两次调用p/invoke。创建一个小型的Java包装器会更有性能,例如:
public void setFooAndBar(int foo, int bar)
{
setFoo(foo);
setBar(bar);
}
翻译为:
public void setFooAndBar(int foo, int bar)
{
setFoo(foo);
setBar(bar);
.NET MAUI视图本质上是c#对象,有很多属性需要在Java中以完全相同的方式设置。如果我们将这个概念应用到.NET MAUI中的每个android View中,我们可以创建一个~18参数的方法用于View创建。后续的属性更改可以直接调用标准的android api。
对于非常简单的.NET MAUI控件来说,这在性能上有了显著的提高:
方法 | 平均 | 错误 | 标准差 | 0代 | 已分配 |
Border(Before) | 323.2 µs | 0.82 µs | 323.2 | 0.9766 | 5 KB |
Border(After) | 242.3 µs | 1.34 µs | 1.25 µs | 0.9766 | 5 KB |
CollectionView(Before) | 354.6 µs | 2.61 µs | 2.31 µs | 1.4648 | 6 KB |
CollectionView(After) | 258.3 µs | 0.49 µs | 0.43 µs | 1.4648 | 6 KB |
请参阅dotnet/maui#3372了解有关此改进的详细信息。
dotnet/maui#3372
https://github.com/dotnet/maui/pull/3372
将android XML移植到Java
回顾android上的dotnet跟踪输出,我们可以看到合理的时间花费在:
20.32.ms mono.andorid!Andorid.Views.LayoutInflater.Inflate
回顾堆栈跟踪,时间实际上花在了android/Java扩展布局上,而在.NET端没有任何工作发生。
如果你看看编译过的android .apk和res/layouts/bottomtablayout。在android Studio中,XML只是普通的XML。只有少数标识符被转换为整数。这意味着android必须解析XML并通过Java的反射api创建Java对象——似乎我们不使用XML就可以获得更快的性能?
通过标准的BenchmarkDotNet对比,我们发现在涉及互操作时,使用android布局的表现甚至比使用c#更差:
方法 | 方法 | 错误 | 标准差 | 已分配 |
Java | 338.4 µs | 4.21 µs | 3.52 µs | 744 B |
CSharp | 410.2 µs | 7.92 µs | 6.61 µs | 1,336 B |
XML | 490.0 µs | 7.77 µs | 7.27 µs | 2,321 B |
接下来,我们将BenchmarkDotNet配置为单次运行,以更好地模拟启动时发生的情况:
方法 | 中值 |
Java | 4.619 ms |
CSharp | 37.337 ms |
XML | 39.364 ms |
我们在.NET MAUI中看到了一个更简单的布局,底部标签导航:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/bottomtab.navarea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="fill"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomtab.tabbar"
android:theme="@style/Widget.Design.BottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
我们可以将其移植到四个Java方法中,例如:
@NonNull
public static List<View> createBottomTabLayout(Context context, int navigationStyle);
@NonNull
public static LinearLayout createLinearLayout(Context context);
@NonNull
public static FrameLayout createFrameLayout(Context context, LinearLayout layout);
@NonNull
public static BottomNavigationView createNavigationBar(Context context, int navigationStyle, FrameLayout bottom)
这使得我们在android上创建底部标签导航时只能从c#切换到Java 4次。它还允许android操作系统跳过加载和解析.xml来“膨胀”Java对象。我们在dotnet/maui中执行了这个想法,在启动时删除所有LayoutInflater.Inflate()调用。
请参阅dotnet/maui#5424, dotnet/maui#5493,和dotnet/maui#5528了解这些改进的详细信息
dotnet/maui#5424
https://github.com/dotnet/maui/pull/5424
dotnet/maui#5493
https://github.com/dotnet/maui/pull/5493
dotnet/maui#5528
https://github.com/dotnet/maui/pull/5528
删除Microsoft.Extensions.Hosting
hosting提供了一个.NET通用主机,用于在.NET应用程序中管理依赖注入、日志记录、配置和应用生命周期。这对启动时间有影响,似乎不适合移动应用程序。
从.NET MAUI中移除Microsoft.Extensions.Hosting使用是有意义的。. net MAUI没有试图与“通用主机”互操作来构建DI容器,而是有自己的简单实现,它针对移动启动进行了优化。此外,. net MAUI默认不再添加日志记录提供程序。
通过这一改变,我们看到dotnet new maui android应用程序的启动时间减少了5-10%。在iOS上,它减少了相同应用程序的大小,从19.2 MB => 18.0 MB。
详见dotnet/maui#4505和dotnet/maui#4545。
.NET通用主机
https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host
dotnet/maui#4505
https://github.com/dotnet/maui/pull/4505
dotnet/maui#4545
https://github.com/dotnet/maui/pull/4545
在启动时减少Shell初始化
Xamarin. Forms Shell是跨平台应用程序导航的一种模式。这个模式是在.NET MAUI中提出的,它被推荐作为构建应用程序的默认方式。
当我们发现在启动时使用Shell的成本(对于Xamarin和Xamarin.form和.NET MAUI),我们找到了几个可以优化的地方:
不要在启动时解析路由——要等到一个需要它们的导航发生。
如果没有为导航提供查询字符串,则只需跳过处理查询字符串的代码。这将删除过度使用System.Reflection的代码路径。
如果页面没有可见的BottomNavigationView,那么不要设置菜单项或任何外观元素。
请参阅dotnet/maui#5262了解此改进的详细信息。
Xamarin. Forms Shell
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/
dotnet/maui#5262
https://github.com/dotnet/maui/pull/5262
字体不应该使用临时文件
大量的时间花在.NET MAUI应用程序加载字体上:
32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3<string, Microsoft.Maui.FontWeight, bool>)
检查代码时,它所做的工作比需要的更多:
将androidAsset文件保存到临时文件夹。
使用android API, Typeface.CreateFromFile()来加载文件。
我们实际上可以直接使用Typeface.CreateFromAsset() android API,根本不用临时文件。
请参阅dotnet/maui#4933了解有关此改进的详细信息。
dotnet/maui#4933
https://github.com/dotnet/maui/pull/4933
编译时在平台上计算
{OnPlatform}标记扩展的使用:
<Label Text="Platform: " />
<Label Text="{OnPlatform Default=Unknown, android=android, iOS=iOS" />
…实际上可以在编译时计算,net6.0-android和net6.0-ios会得到适当的值。在未来的.NET版本中,我们将对 XML元素进行同样的优化。
详见dotnet/maui#4829和dotnet/maui#5611。
dotnet/maui#4829
https://github.com/dotnet/maui/pull/4829
dotnet/maui#5611
https://github.com/dotnet/maui/pull/5611
在XAML中使用编译转换器
以下类型现在在XAML编译时转换,而不是在运行时:
颜色:dotnet /maui# 4687
https://github.com/dotnet/maui/pull/4687
角半径: dotnet / maui # 5192
https://github.com/dotnet/maui/pull/5192
字形大小:dotnet / maui # 5338
https://github.com/dotnet/maui/pull/5338
网格长度, 行定义, 列定义:dotnet/maui#5489
https://github.com/dotnet/maui/pull/5489
这导致从.xaml文件生成更好/更快的IL。
优化颜色解析
Microsoft.Maui.Graphics.Color.Parse()的原始代码可以重写,以更好地使用Span并避免字符串分配。
方法 | 平均 | 错误 | 标准差 | 0代 | 已分配 |
Parse (之前) | 99.13 ns | 0.281 ns | 0.235 ns | 0.0267 | 168 B |
Parse (之后) | 52.54 ns | 0.292 ns | 0.259 ns | 0.0051 | 32 B |
能够在ReadonlySpan<char>dotnet/csharplang#1881上使用switch语句,将在未来的.NET版本中进一步改善这种情况。
看到dotnet / Microsoft.Maui.Graphics # 343和dotnet / Microsoft.Maui.Graphics # 345关于这个改进的细节。
dotnet/csharplang#1881
https://github.com/dotnet/csharplang/issues/1881
dotnet / Microsoft.Maui.Graphics # 343
https://github.com/dotnet/Microsoft.Maui.Graphics/pull/343
dotnet / Microsoft.Maui.Graphics # 345
https://github.com/dotnet/Microsoft.Maui.Graphics/pull/345
不要使用区域性识别的字符串比较
回顾一个新的naui项目的dotnet跟踪输出,可以看到android上第一个区域性感知字符串比较的真实成本:
6.32ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellNavigationManager.GetNavigationState
3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri
3.82ms System.Private.CoreLib!System.String.StartsWith
2.57ms System.Private.CoreLib!System.Globalization.CultureInfo.get_CurrentCulture
实际上,我们甚至不希望在本例中使用区域性比较—它只是从Xamarin.Forms引入的代码。
例如,如果你有:
if (text.StartsWith("f"))
{
// do something
}
在这种情况下,你可以简单地这样做:
if (text.StartsWith("f"))
{
// do something
}
如果在整个应用程序中执行,System.Globalization.CultureInfo.CurrentCulture可以避免被调用,并且可以稍微提高If语句的总体速度。
为了解决整个dotnet/maui回购的这种情况,我们引入了代码分析规则来捕捉这些:
dotnet_diagnostic.CA1307.severity = error
dotnet_diagnostic.CA1309.severity = error
请参阅dotnet/maui#4988了解有关改进的详细信息。
dotnet/maui#4988
https://github.com/dotnet/maui/pull/4988
懒惰地创建日志
ConfigureFonts() API在启动时花费了一些时间来做一些可以延迟到以后的工作。我们还可以改进Microsoft.Extensions中日志基础设施的一般用法。
我们所做的一些改进如下:
推迟创建“记录器”类,直到需要它们时再创建。
内置的日志记录基础设施在默认情况下是禁用的,必须显式启用。
延迟调用android的EmbeddedFontLoader中的Path.GetTempPath(),直到需要它。
不要使用ILoggerFactory创建通用记录器。而是直接获取ILogger服务,这样它就被缓存了。
请参阅dotnet/maui#5103了解有关此改进的详细信息。
dotnet/maui#5103
https://github.com/dotnet/maui/pull/5103
使用工厂方法进行依赖注入
当使用Microsoft.Extensions。DependencyInjection,注册服务,比如:
IServiceCollection services /* ... */;
services.TryAddSingleton<IFooService, FooService>();
Microsoft.Extensions必须做一些System.Reflection来创建FooService的第一个实例。这是值得注意的dotnet跟踪输出在android上。
相反,如果你这样做了:
// If FooService has no dependencies
services.TryAddSingleton<IFooService>(sp => new FooService());
// Or if you need to retrieve some dependencies
services.TryAddSingleton<IFooService>(sp => new FooService(sp.GetService<IBar>()));
在这种情况下,Microsoft.Extensions可以简单地调用lamdba/匿名方法,而不需要系统。反射。
我们在所有的dotnet/maui上进行了改进,并使用了bannedapianalyzer,这样就不会有人意外地使用TryAddSingleton()更慢的重载。
请参阅dotnet/maui#5290了解有关此改进的详细信息。
bannedapianalyzer
https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md
dotnet/maui#5290
https://github.com/dotnet/maui/pull/5290
默认VerifyDependencyInjectionOpenGenericServiceTrimmability
.NET Podcast样本花费了4-7ms的时间:
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()
MSBuild属性$(verifydependencyinjectionopengenericservicetrimability)触发该方法运行。这个特性开关确保dynamallyaccessedmembers被正确地应用于打开依赖注入中的泛型类型。
在基础.NET SDK中,当publishtrim =true时,该开关将被启用。然而,android应用程序在Debug版本中并没有设置publishtrim =true,所以开发者错过了这个验证。
相反,在已发布的应用程序中,我们不想支付这种验证的成本。所以这个特性开关应该在Release版本中关闭。
查看xamarin-android#6727和xamarin-macios#14130了解关于这个改进的详细信息。
.NET Podcast
https://github.com/dotnet/runtime/pull/65326
xamarin-android#6727
https://github.com/xamarin/xamarin-android/pull/6727
xamarin-macios#14130
https://github.com/xamarin/xamarin-macios/pull/14130
懒惰地负载ConfigurationManager
configurationmanager并没有被许多移动应用程序使用,而且创建一个是非常昂贵的!(例如,在android上约为7.59ms)
在.NET MAUI中,一个ConfigurationManager在启动时默认被创建,我们可以使用Lazy延迟它的创建,所以它将不会被创建,除非请求。
请参阅dotnet/maui#5348了解有关此改进的详细信息。
dotnet/maui#5348
https://github.com/dotnet/maui/pull/5348
改进内置AOT配置文件
Mono运行时有一个关于每个方法的JIT时间的报告(参见我们的文档),例如:
Total(ms) | Self(ms) | 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报告:
_ = new Microsoft.Maui.Layouts.GridLayoutManager(new Grid()).Measure(100, 100);
<SolidColorBrush x:Key="ProfiledAot_AppThemeBinding_Color" Color="{AppThemeBinding Default=Black}"/>
<CollectionView x:Key="ProfiledAot_CollectionView_OnIdiom_Thickness" Margin="{OnIdiom Default=1,1,1,1}" />
这导致了进一步的补充:
var split = "foo;bar".Split(';');
var x = int.Parse("999");
x.ToString();
我们对Color.Parse()、Connectivity做了类似的修改.NETworkAccess DeviceInfo。成语,AppInfo。.NET MAUI应用程序中应该经常使用的requestdtheme。
请参阅dotnet/maui#5559, dotnet/maui#5682,和dotnet/maui#6834了解这些改进的详细信息。
如果你想在.NET 6中记录一个自定义的AOT配置文件,你可以尝试我们的实验包Mono.Profiler.Android。我们正在努力在未来的.NET版本中完全支持记录自定义概要文件。
参见我们的文档
https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/profiling.md#profiling-the-jit-compiler
.NET Podcast
https://github.com/microsoft/dotnet-podcasts
dotnet/maui#5559
https://github.com/dotnet/maui/pull/5559
dotnet/maui#5682
https://github.com/dotnet/maui/pull/5682
dotnet/maui#6834
https://github.com/dotnet/maui/pull/6834
Mono.Profiler.Android
https://github.com/jonathanpeppers/Mono.Profiler.Android
启用AOT图像的延迟加载
以前,Mono运行时将在启动时加载所有AOT图像,以验证托管.NET程序集(例如Foo.dll)的MVID是否与AOT图像(libFoo.dll.so)匹配。在大多数.NET应用程序中,一些AOT映像可能稍后才需要加载。
Mono中引入了一个新的——aot-lazy-assembly-load或mono_opt_aot_lazy_assembly_load设置,android工作负载可以选择。我们发现这将dotnet new maui项目在Pixel 6 Pro上的启动时间提高了约25ms。
这是默认启用的,但如果需要,你可以在你的。csproj中通过以下方式禁用此设置:
<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>
查看dotnet/runtime#67024和xamarin-android #6940了解这些改进的详细信息。
dotnet/runtime#67024
https://github.com/dotnet/runtime/pull/67024
xamarin-android #6940
https://github.com/xamarin/xamarin-android/pull/6940
删除System.Uri中未使用的编码对象
一个MAUI应用程序的dotnet跟踪输出,显示大约7ms花费了加载UTF32和Latin1编码的第一次系统。使用Uri api:
<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>
这个字段是不小心留在原地的。只需删除s_noFallbackCharUTF8字段,就可以改进任何使用System.Uri 或相关的api的. net应用程序的启动。
参见dotnet/runtime#65326了解有关此改进的详细信息。
dotnet/runtime#65326
https://github.com/dotnet/runtime/pull/65326
应用程序大小的改进
修复默认的MauiImage大小
dotnet new maui模板显示一个友好的"网络机器人”的形象。这是通过使用一个.svg文件作为一个MauiImage和内容来实现的:
<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- everything else -->
默认情况下,MauiImage使用.svg中的宽度和高度值作为图像的“基础大小”。回顾构建输出,这些图像被缩放为:
objReleasenet6.0-androidresizetizerrmipmap-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#4759和dotnet/maui#6419了解这些改进的细节。
dotnet/maui#4759
https://github.com/dotnet/maui/pull/4759
dotnet/maui#6419
https://github.com/dotnet/maui/pull/6419
删除Application.Properties 和DataContractSerializer
Xamarin.Forms 有一个 API,用于通过 Application.Properties 字典持久化键值对。这在内部使用了DataContractSerializer,这对于自包含和修剪的移动应用程序不是最佳选择。来自BCL的System.Xml的部分可能相当大,我们不想在每个.NET MAUI应用程序中都为此付出代价。
简单地删除这个API和所有DataContractSerializer的使用,在android上可以提高约855KB,在iOS上提高约1MB。
请参阅dotnet/maui#4976了解有关此改进的详细信息。
dotnet/maui#4976
https://github.com/dotnet/maui/pull/4976
修剪未使用的HTTP实现
System.NET.Http.UseNativeHttpHandler没有适当地削减底层托管HTTP处理程序(SocketsHttpHandler)。默认情况下,androidMessageHandler和NSUrlSessionHandler被用来利用底层的android和iOS网络栈。
通过修正这个问题,在任何.NET MAUI应用程序中都可以删除更多的IL代码。在一个例子中,一个使用HTTP的android应用程序能够完全删除几个程序集:
Microsoft.Win32.Primitives.dll
System.Formats.Asn1.dll
System.IO.Compression.Brotli.dll
System.NET.NameResolution.dll
System.NET.NETworkInformation.dll
System.NET.Quic.dll
System.NET.Security.dll
System.NET.Sockets.dll
System.Runtime.InteropServices.RuntimeInformation.dll
System.Runtime.Numerics.dll
System.Security.Cryptography.Encoding.dll
System.Security.Cryptography.X509Certificates.dll
System.Threading.Channels.dll
查看dotnet/runtime#64852, xamarin-android#6749,和xamarin-macios#14297关于这个改进的详细信息。
dotnet/runtime#64852
https://github.com/dotnet/runtime/pull/64852
xamarin-android#6749
https://github.com/xamarin/xamarin-android/pull/6749
xamarin-macios#14297
https://github.com/xamarin/xamarin-macios/pull/14297
.NET Podcast示例中的改进
我们对样本本身做了一些调整,其中更改被认为是“最佳实践”。
删除Microsoft.Extensions.Http用法
使用Microsoft.Extensions.Http对于移动应用程序来说太重了,并且在这种情况下没有提供任何真正的价值。
因此,HttpClient不使用DI:
builder.Services.AddHttpClient<ShowsService>(client =>
{
client.BaseAddress = new Uri(Config.APIUrl);
});
// Then in the service ctor
public ShowsService(HttpClient httpClient, ListenLaterService listenLaterService)
{
this.httpClient = httpClient;
// ...
}
我们简单地创建一个HttpClient来在服务中使用:
public ShowsService(ListenLaterService listenLaterService)
{
this.httpClient = new HttpClient() { BaseAddress = new Uri(Config.APIUrl) };
// ...
}
我们建议对应用程序需要交互的每个web服务使用一个单独的HttpClient实例。
请参阅dotnet/runtime#66863和dotnet podcasts#44了解有关改进的详细信息。
dotnet/runtime#66863
https://github.com/dotnet/runtime/issues/66863
dotnet podcasts#44
https://github.com/microsoft/dotnet-podcasts/pull/44
删除Newtonsoft.Json使用
.NET Podcast 样本使用了一个名为MonkeyCache的库,它依赖于Newtonsoft.Json。这本身并不是一个问题,只是.NET MAUI + Blazor应用程序依赖于一些ASP.NET Core库反过来依赖于System.Text.Json。这款应用实际上是为JSON解析库“付了两倍钱”,这对应用的大小产生了影响。
我们移植了MonkeyCache 2.0来使用System.Text。Json,不需要Newtonsoft。这将iOS上的应用大小从29.3MB减少到26.1MB!
参见monkey-cache#109和dotnet-podcasts#58了解有关改进的详细信息。
.NET Podcast
https://github.com/microsoft/dotnet-podcasts
MonkeyCache
https://github.com/jamesmontemagno/monkey-cache
monkey-cache#109
https://github.com/jamesmontemagno/monkey-cache/pull/109
dotnet-podcasts#58
https://github.com/microsoft/dotnet-podcasts/pull/58
在后台运行第一个网络请求
回顾dotnet跟踪输出,初始请求在ShowsService阻塞UI线程初始化连接.NETworkAccess Barrel.Current。得到,HttpClient。这项工作可以在后台线程中完成-在这种情况下导致更快的启动时间。在Task.Run()中封装第一个调用,可以在一定程度上提高这个示例的启动效率。
在Pixel 5a设备上平均运行10次:
Before
Average(ms): 843.7
Average(ms): 847.8
After
Average(ms): 817.2
Average(ms): 812.8
对于这种类型的更改,总是建议根据dotnet跟踪或其他分析结果来做出决定,并度量更改前后的变化。
请参阅dotnet-podcasts#57了解有关此改进的详细信息。
dotnet-podcasts#57
https://github.com/microsoft/dotnet-podcasts/pull/57
实验性或高级选项
如果你想在android上进一步优化你的.NET MAUI应用程序,这里有一些高级或实验性的特性,默认情况下不是启用的。
修剪Resource.designer.cs
自从Xamarin诞生以来,android应用程序就包含了一个生成的Properties/Resource.designer.cs文件,用于访问androidResource文件的整数标识符。这是R.java类的c# /托管版本,允许使用这些标识符作为普通的c#字段(有时是const),而无需与Java进行任何互操作。
在一个android Studio“库”项目中,当你包含一个像res/drawable/foo.png这样的文件时,你会得到一个像这样的字段:
package com.yourlibrary;
public class R
{
public class drawable
{
// The actual integer here maps to a table inside the final .apk file
public final int foo = 1234;
}
}
你可以使用这个值,例如,在ImageView中显示这个图像:
ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.foo);
当你构建com.yourlibrary.aar时, android的gradle插件实际上并没有把这个类放在包中。相反,android应用程序实际上知道整数的值是多少。因此,R类是在android应用程序构建时生成的,为每个android库生成一个R类。
Xamarin.Android采取了不同的方法,在运行时进行整数修复。用c#和MSBuild做这样的事情真的没有一个很好的先例吗?例如,一个c# android库可能有:
public class Resource
{
public class Drawable
{
// The actual integer here is *not* final
public int foo = -1;
}
}
然后主应用程序就会有如下代码:
public class Resource
{
public class Drawable
{
public Drawable()
{
// Copy the value at runtime
global::MyLibrary.Resource.Drawable.foo = foo;
}
// The actual integer here *is* final
public const int foo = 1234;
}
}
这种情况已经很好地运行了一段时间,但不幸的是,像androidX、Material、谷歌Play Services等谷歌的库中的资源数量已经开始复合。例如,在dotnet/maui#2606中,启动时设置了21497个字段!我们创建了一种方法来解决这个问题,但我们也有一个新的自定义修剪步骤来执行修复在构建时(在修剪期间)而不是在运行时。
<AndroidLinkResources>true</ AndroidLinkResources>
这将使你的版本版本替换案例如下:
ImageView imageView = new(this);
imageView.SetImageResource(Resource.Drawable.foo);
相反,直接内联整数:
ImageView imageView = new(this);
imageView.SetImageResource(1234); // The actual integer here *is* final
这个特性的一个已知问题是:
public partial class Styleable
{
public static int[] ActionBarLayout = new int[] { 16842931 };
}
目前不支持替换int[]值,这使得我们不能默认启用它。一些应用程序将能够打开这个功能,dotnet新的maui模板,也许许多.NET maui android应用程序不会遇到这个限制。
在未来的.NET版本中,我们可能会默认启用$(androidLinkResources),或者完全重新设计。
查看xamarin-android#5317, xamarin-android#6696,和dotnet/maui#4912了解该功能的详细信息。
dotnet/maui#2606
https://github.com/dotnet/maui/pull/2606
xamarin-android#5317
https://github.com/xamarin/xamarin-android/pull/5317
xamarin-android#6696
https://github.com/xamarin/xamarin-android/pull/6696
dotnet/maui#4912
https://github.com/dotnet/maui/pull/4912
R8 Java代码收缩器
R8是全程序优化、收缩和缩小工具,将java字节代码转换为优化的dex代码。R8使用Proguard keep规则格式为应用程序指定入口点。如您所料,许多应用程序需要额外的Proguard规则来保持工作。R8可能过于激进,并且删除了Java反射所调用的一些东西,等等。我们还没有一个很好的方法让它成为所有.NET android应用程序的默认设置。
要选择使用R8 for Release版本,请在你的.csproj中添加以下内容:
<!-- NOTE: not recommended for Debug builds! -->
<AndroidLinkTool Condition="'$(Configuration)' == 'Release'">r8</AndroidLinkTool>
如果启动你的应用程序的Release构建在启用后崩溃,检查adb logcat输出,看看哪里出了问题。
如果你看到java.lang. classnotfoundexception或java.lang。你可能需要添加一个ProguardConfiguration文件到你的项目中,比如:
<ItemGroup>
<ProguardConfiguration Include="proguard.cfg" />
</ItemGroup>
-keep class com.thepackage.TheClassYouWantToPreserve { *; <init>(...); }
我们正在研究在未来的.NET版本中默认启用R8的选项。
详情请参阅我们的D8/R8文档。
我们的D8/R8文档
https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/D8andR8.md
AOT
Profiled AOT是默认的,因为它在应用程序大小和启动性能之间给出了最好的权衡。如果应用程序的大小与你的应用程序无关,你可以考虑对所有.NET程序集使用AOT。
要选择加入,在你的.csproj中添加以下Release配置:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<androidEnableProfiledAot>false</androidEnableProfiledAot>
</PropertyGroup>
这将减少在应用程序启动期间发生的JIT编译量,以及导航到后面的屏幕等。
AOT和LLVM
LLVM提供了一个独立于源和目标的现代优化器,可以与Mono AOT Compiler输出相结合。其结果是,应用的尺寸略大,发行构建时间更长,运行时性能更好。
要选择将LLVM用于Release版本,请将以下内容添加到你的.csproj中:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<EnableLLVM>true</EnableLLVM>
</PropertyGroup>
此特性可以与Profiled AOT(或AOT-ing一切)结合使用。对比应用程序的前后,了解EnableLLVM对应用程序大小和启动性能的影响。
目前,需要安装一个android NDK来使用这个功能。如果我们能够解决这个需求,EnableLLVM将成为未来.NET版本中的默认选项。
有关详细信息,请参阅我们关于EnableLLVM的文档。
LLVM
https://llvm.org/
EnableLLVM的文档
https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties
记录自定义AOT配置文件
概要AOT默认使用我们在.NET MAUI和android工作负载中提供的“内置”概要文件,对大多数应用程序都很有用。为了获得最佳的启动性能,理想情况下应该记录应用程序特定的配置文件。针对这种情况,我们有一个实验性的Mono.Profiler.Android包。
记录配置文件:
dotnet add package Mono.AotProfiler.android
dotnet build -t:BuildAndStartAotProfiling
# Wait until app launches, or you navigate to a screen
dotnet build -t:FinishAotProfiling
这将在你的项目目录下产生一个custom.aprof。要在未来的构建中使用它:
<ItemGroup>
<androidAotProfile Include="custom.aprof" />
</ItemGroup>
我们正在努力在未来的.NET版本中完全支持记录自定义概要文件。
Mono.Profiler.Android
https://github.com/jonathanpeppers/Mono.Profiler.Android
结论
我希望您喜欢我们的.NET MAUI性能论述。请尝试.NET MAUI并且可以在http://dot.net/maui了解更多!
边栏推荐
- After the uniapp jumps to the page in onlaunch, click the event failure solution
- 软件设计之——“高内聚低耦合”
- 對比學習之 Unsupervised Learning of Visual Features by Contrasting Cluster Assignments
- RationalDMIS2022 高级编程宏程序
- Process control (creation, termination, waiting, program replacement)
- 基于DE2 115开发板驱动HC_SR04超声波测距模块【附源码】
- 【愚公系列】2022年7月 Go教学课程 005-变量
- [encapsulation of time format tool functions]
- Wallhaven wallpaper desktop version
- 分布式数据库主从配置(MySQL)
猜你喜欢
[untitled]
Vscode 尝试在目标目录创建文件时发生一个错误:拒绝访问【已解决】
Use metersphere to keep your testing work efficient
Using ENSP to do MPLS pseudo wire test
[C #] the solution of WinForm operation zoom (blur)
关于在云服务器上(这里用腾讯云)安装mysql8.0并使本地可以远程连接的方法
Interprocess communication (IPC)
How to add aplayer music player in blog
Web端自动化测试失败的原因
The database synchronization tool dbsync adds support for mongodb and es
随机推荐
Android 面试知识点
uniCloud
verilog设计抢答器【附源码】
浙江大学周亚金:“又破又立”的顶尖安全学者,好奇心驱动的行动派
LeetCode - 面试题17.24 最大子矩阵
Leetcode - interview question 17.24 maximum submatrix
Input type= "password" how to solve the problem of password automatically brought in
Array object sorting
常用sql语句整理:mysql
RationalDMIS2022 高级编程宏程序
Verilog realizes nixie tube display driver [with source code]
Kitex 重试机制
Network foundation (1)
TDengine 社区问题双周精选 | 第二期
基于Retrofit框架的金山API翻译功能案例
PostgreSQL中的表复制
0.96 inch IIC LCD driver based on stc8g1k08
基于华为云IOT设计智能称重系统(STM32)
聊聊SOC启动(九) 为uboot 添加新的board
Briefly introduce closures and some application scenarios