.NET MAUI的性能改进说明和使用教程

1,616 阅读21分钟

.NET多平台应用程序用户界面(MAUI)将Android、iOS、macOS和Windows的API统一为一个单一的API,因此你可以编写一个应用程序,并在许多平台上原生运行。我们专注于提高你的日常生产力以及你的应用程序的性能。我们认为,开发者生产力的提高不应该以牺牲应用程序的性能为代价。

对于应用程序的规模也可以这样说--在一个空白的.NET MAUI应用程序中存在哪些开销?当我们开始优化.NET MAUI时,很明显,iOS需要做一些工作来改善应用程序的大小,而Android则在启动性能方面有所欠缺。

一个dotnet new maui 项目的iOS应用程序大小最初大约是18MB。同样,在早期预览中,Android上的.NET MAUI启动时间也不太理想。

应用程序框架启动时间(ms)
Xamarin.AndroidXamarin306.5
Xamarin.FormsXamarin498.6
Xamarin.Forms (Shell)Xamarin817.7
dotnet新安卓.NET 6210.5
dotnet new maui.NET 6683.9
.NET播客.NET 61299.9

这是在Pixel 5设备上运行10次的平均值。关于这些数字是如何获得的,请参见我们的maui-profilingrepo。

我们的目标是让.NET MAUI比其前身Xamarin.Forms更快,很明显,我们在.NET MAUI本身有一些工作要做。dotnet new android 模板已经形成了比Xamarin.Android更快的启动速度,这主要是由于.NET 6中新的BCL和Mono运行时间。

dotnet new maui 模板还没有使用Shell导航模式,但计划将其作为.NET MAUI的默认导航模式。我们知道,当我们采用这一变化时,模板的性能会受到影响。

为了达到我们今天的目标,我们与几个不同的团队合作。我们改进了Microsoft.Extensions和DependencyInjection的使用、AOT编译、Java互操作、XAML、.NET MAUI中的代码等领域。

尘埃落定后,我们来到了一个更好的地方。

应用程序框架启动时间(ms)
dotnet new android.net 6 / Maui ga182.8
dotnet new maui.net 6 / Maui ga568.1
dotnet新毛伊岛**.net 6 / Maui ga464.2
.NET播客.NET 6 / Maui ga814.2

目录

启动性能的改进

应用程序大小的改进

.NET播客样本的改进

实验性或高级选项

启动时的性能改进

在移动平台上进行剖析

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

剖析.NET 6 Android应用程序需要使用一个叫做dotnet-dsrouter.这个工具使dotnet trace ,以连接到Android、iOS等系统上运行的移动应用程序。这可能是我们用于剖析.NET MAUI的最有影响的工具。

要开始使用dotnet tracedsrouter ,首先通过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 trace ,例如。

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

启动用-c Release-p:AndroidEnableProfiler=true 构建的安卓应用后,你会注意到dotnet trace 输出时的连接。

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

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

speedscope view

speedscope view

有关在Android应用程序中使用dotnet trace 的进一步细节,请参见我们的文档。我建议在物理的Android设备上对Release 构建进行分析,以获得你的应用程序的真实性能的最佳画面。

随着时间的推移进行测量

我们在.NET基本原理团队的朋友设置了一个管道来跟踪.NET MAUI的性能情况,例如:

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

这使我们能够看到随着时间的推移,改进或退步的影响,看到dotnet/maui repo的每个提交的数字。我们还可以确定差异是由xamarin-android、xamarin-macios或dotnet/runtime的变化造成的。

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

graph of startup .NET MAUI template

请注意,Pixel 4a的速度比Pixel 5慢得多。

我们可以准确地指出dotnet/maui中发生退步和改进的提交。这对追踪我们的目标有多大的帮助,怎么强调都不为过。

同样,我们可以看到我们在同一台Pixel 4a设备上的.NET播客应用程序的进展。

graph of startup .NET Podcast sample

这张图是我们真正关注的焦点,因为它是一个 "真实的应用程序",接近开发者在自己的移动应用程序中看到的情况。

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

graph of app size .NET Podcast sample

参见dotnet-podcasts#58AndroidX#520dotnet/maui#6419了解这些改进的细节。

剖析AOT

在我们对.NET MAUI的最初性能测试中,我们看到了JIT(及时)与AOT(提前)编译的代码的性能。

应用程序JIT时间(ms)AOT时间(毫秒)
dotnet new maui1078.0ms683.9ms

JIT-ing发生在每个C#方法被调用的第一时间,这隐含地影响了移动应用程序的启动性能。

同样有问题的是由AOT引起的应用程序大小的增加。每一个.NET程序集都会在最终的应用程序中添加一个Android本地库。为了两全其美,启动跟踪或剖析AOT是Xamarin.Android的一个当前功能。这是一种对应用程序的启动路径进行AOT的机制,它可以大大改善启动时间,而应用程序的大小只需适度增加。

在.NET 6中,将其作为Release构建的默认选项是完全合理的。过去,使用Xamarin.Android进行任何形式的AOT都需要Android NDK(下载量达数千字节)。我们在没有安装Android NDK的情况下,为构建AOT的应用程序做了大量的工作,使其有可能成为未来的默认选项。

我们为dotnet new androidmauimaui-blazor 模板录制了内置的配置文件,使大多数应用程序受益。如果你想在.NET 6中记录一个自定义的配置文件,你可以试试我们实验性的Mono.Profiler.Android包。我们正在努力争取在未来的.NET版本中完全支持记录自定义配置文件。

参见xamarin-android#6547dotnet/maui#4859,以了解有关这一改进的细节。

单一文件的汇编存储

以前,如果你在你喜欢的压缩文件工具中查看Release Android.apk 内容,你可以看到.NET装配位于:

assembliesJava.Interop.dll
assembliesMono.Android.dll
assembliesSystem.Runtime.dll
assembliesarm64-v8aSystem.Private.CoreLib.dll
assembliesarmeabi-v7aSystem.Private.CoreLib.dll
assembliesx86System.Private.CoreLib.dll
assembliesx86_64System.Private.CoreLib.dll

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

在这个改变之后,你的结局是:

assembliesassemblies.manifest
assembliesassemblies.blob
assembliesassemblies.arm64_v8a.blob
assembliesassemblies.armeabi_v7a.blob
assembliesassemblies.x86.blob
assembliesassemblies.x86_64.blob

现在安卓系统的启动只需要调用 mmap两次:一次为assemblies.blob ,第二次为架构特定的blob。这对有许多.NET程序集的应用程序产生了明显的影响。

如果你需要从一个已编译的Android应用程序中检查这些程序集的IL,我们创建了一个程序集存储阅读器工具来 "解压 "这些文件。

另一个选择是在构建你的应用程序时禁用这些设置:

donut build -c Release -p:AndroidUseAssemblyStore=false -p:AndroidEnableAssemblyCompression=false

这使你能够用你最喜欢的压缩工具解压所得的.apk ,并用ILSpy这样的工具来检查.NET程序集。这是一个诊断修剪器/链接器问题的好方法。

关于这个改进的细节,请看xamarin-android#6311

Spanify RegisterNativeMembers

当一个C#对象从Java创建时,会调用一个小的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_Handlern";
        mono.android.Runtime.register ("foo.MainActivity, foo", MainActivity.class, methods);
    }

methods 列表是一个n:-限定的Java本地接口(JNI)签名列表,这些签名在托管的C#代码中被重写。你为每个在C#中被重写的Java方法得到其中一个。

当实际的JavaonCreate() 方法被调用时,对于一个AndroidActivity

public void onCreate (android.os.Bundle p0)
{
    n_onCreate (p0);
}

private native void n_onCreate (android.os.Bundle p0);

通过各种握手和挥手,n_onCreate 调用到Mono运行时并调用我们在C#中的OnCreate() 方法。

分割n:-限定方法列表的代码是在Xamarin的早期使用string.Split() 。足以说明 Span<T>那时还不存在,但我们现在可以使用它了!这提高了任何C#类对Java类的子类的成本,所以它是一个更广泛的改进,而不仅仅是.NET MAUI。

你可能会问,"为什么要使用字符串呢?"使用Java数组似乎比限定字符串的性能影响更差。在我们的测试中,调用JNI来获得Java数组元素,比string.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<IntPtr, object?[]?>) method.CreateDelegate (typeof (Action <IntPtr, object []>));

我们调用返回的委托,这样,IntPtrJava.Lang.Object 子类的Handleobject[] 是该特定C#构造器的任何参数。System.Reflection.Emit在启动时的第一次使用以及以后的每次调用都有很大的代价。

经过仔细审查,我们可以将handle 字段internal ,并将这段代码简化为:

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);

这段代码所做的是在不调用构造函数的情况下创建一个对象(好奇怪吗?),设置handle 字段,然后调用构造函数。这样做是为了在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 应用程序来说,这段代码被调用了两次,但对于一个dotnet new maui 应用程序来说,调用了~58次!

取而代之的是使用System.Reflection.Emit,我们意识到我们实际上可以为每个常见的委托类型编写一个强类型的 "快速路径"。 有一个生成的delegate ,与每个签名匹配。

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-size的增长。我们正在研究如何在未来的.NET版本中改进这一点。

参见xamarin-android#6657xamarin-android#6707,了解有关这一改进的细节。

较新的Java.Interop APIs

Java.Interop.dll ,最初的Xamarin APIs是这样的API。

  • JNIEnv.CallStaticObjectMethod

其中调用到Java的 "新方法 "使每次调用的内存分配减少。

  • JniEnvironment.StaticMethods.CallStaticObjectMethod

在构建时为Java方法生成C#绑定时,默认使用较新/较快的方法--在Xamarin.Android中已经有一段时间了。以前,Java绑定项目可以将其设置为$(AndroidCodegenTarget)XAJavaInterop1 ,这在每次调用时都会缓存和重用jmethodID 实例。关于这个功能的历史,请参见java.interoprepo。

剩下的地方,这是一个问题,是我们有 "手动 "绑定的地方。这些往往也是经常使用的方法,所以值得去修复这些方法!

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

多维的Java数组

当把C#数组来回传递给Java时,一个中间步骤必须复制数组,以便适当的运行时能够访问它。这确实是一个开发人员的经验情况,因为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图像使用Glide

Glide是现代Android应用程序的推荐图像加载库。Google文档甚至推荐使用它,因为内置的AndroidBitmap 类很难被正确使用。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#中被子类化,以处理托管代码中的完成。其结果是,来自网络的图像的性能应该比你以前在Xamarin.Forms中得到的有明显的改善。

参见dotnet/maui#759dotnet/maui#5198,以了解有关这一改进的细节。

减少Java互操作的调用

假设你有以下的Java APIs:

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中的每一个AndroidView ,我们就可以创建一个~18个参数的方法,在View 创建时使用。随后的属性变化可以可以直接调用标准的Android APIs。

这在性能上有很大的提高,即使是非常简单的.NET MAUI控件。

方法平均值误差StdDevGen 0分配的
边界(之前)323.2 微秒0.82 µs0.68 微秒0.97665 KB
边界(后)242.3 微秒1.34 µs1.25 微秒0.97665 KB
内容视图(之前)354.6 µs2.61 µs2.31 微秒1.46486 KB
内容视图(后)258.3 µs0.49 µs0.43 微秒1.46486 KB

有关这一改进的细节,请参见dotnet/maui#3372

将Android的XML移植到Java

回顾Android上的dotnet trace 输出,我们可以看到有合理的时间花费:

20.32.ms mono.android!Android.Views.LayoutInflater.Inflate

回顾堆栈跟踪,时间实际上花在了Android/Java中,以膨胀布局,而在.NET侧没有发生任何工作。

如果你看一下在Android Studio中编译的Android.apkres/layouts/bottomtablayout.axml ,XML只是普通的XML。只有少数标识符被转换为整数。这意味着Android必须解析这个XML,并通过Java的反射API创建Java对象--似乎我们可以通过使用XML获得更快的性能?

测试一个标准的BenchmarkDotNet比较,我们发现,在涉及到互操作时,使用Android布局的表现甚至比C#更差。

方法平均值误差StdDev已分配
爪哇338.4 微秒4.21 µs3.52 微秒744 B
CSharp410.2 µs7.92 µs6.61 微秒1,336 B
XML490.0 微秒7.77 µs7.27 微秒2,321 B

接下来,我们将BenchmarkDotNet配置为单次运行,以更好地模拟启动时的情况。

方法平均值
Java4.619 ms
CSharp37.337 ms
XML39.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四次。它还允许Android操作系统跳过加载和解析一个.xml ,以 "膨胀 "Java对象。我们把这个想法贯穿于dotnet/maui中,在启动时删除了所有LayoutInflater.Inflate() 的调用。

关于这些改进的细节,请参见dotnet/maui#5424,dotnet/maui#5493, anddotnet/maui#5528

删除Microsoft.Extensions.Hosting

Microsoft.Extensions.Hosting 提供了一个.NET通用主机,用于管理.NET应用程序中的依赖注入、日志、配置和应用程序生命周期。这对启动时间有影响,对于移动应用程序来说似乎并不合适。

从.NET MAUI中删除Microsoft.Extensions.Hosting 的使用是有意义的。与其试图与 "通用主机 "互操作以建立DI容器,.NET MAUI有自己的简单实现,为移动启动进行了优化。此外,.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.Forms和.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

使用{OnPlatform} 标记扩展。

<Label Text="Platform: " />
<Label Text="{OnPlatform Default=Unknown, Android=Android, iOS=iOS" />

...实际上可以在编译时计算,其中net6.0-androidnet6.0-ios 得到适当的值。在未来的.NET版本中,我们将研究对<OnPlatform/> XML元素的同样优化。

有关这一改进的细节,请参见dotnet/maui#4829dotnet/maui#5611

在XAML中使用编译的转换器

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

这样就能从.xaml 文件中更好/更快地生成IL。

优化颜色解析

Microsoft.Maui.Graphics.Color.Parse() 的原始代码可以重写,以更好地利用Span<T> 并避免字符串分配。

方法平均值误差StdDev0基因已分配
解析(之前)99.13 ns0.281 ns0.235 ns0.0267168 B
解析(后)52.54 ns0.292 ns0.259 ns0.005132 B

能够在ReadonlySpan<char>dotnet/csharplang#1881上使用switch-statement,在未来的.NET版本中会进一步改善这种情况。

参见dotnet/Microsoft.Maui.Graphics#343dotnet/Microsoft.Maui.Graphics#345,以了解有关这一改进的详情。

不要使用具有文化意识的字符串比较

回顾dotnet trace ,一个dotnet new maui 项目的输出显示了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 repo中的这种情况,我们引入了代码分析规则来捕捉这些情况:

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<IFooService, FooService>();

Microsoft.Extensions需要做一些System.Reflection来创建FooService 的第一个实例。这在Android上的dotnet trace 输出中很明显。

相反,如果你做:

// 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/anonymous方法,不涉及System.Reflection。

我们在所有的dotnet/maui中都做了这个改进,同时也使用了 BannedApiAnalyzers,这样就不会有人意外地使用TryAddSingleton() 的慢速重载。

关于这一改进的细节,请参见dotnet/maui#5290

默认的VerifyDependencyInjectionOpenGenericServiceTrimmability

.NET播客样本在花了4-7ms的时间:

Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()

$(VerifyDependencyInjectionOpenGenericServiceTrimmability)MSBuild属性会触发此方法的运行。这个功能开关确保DynamicallyAccessedMembers ,正确应用于依赖注入中使用的开放通用类型。

在基本的.NET SDK中,这个开关在PublishTrimmed=true 。然而,Android应用程序在Debug 构建中不设置PublishTrimmed=true ,所以开发者错过了这个验证。

相反,在发布的应用程序中,我们不想支付做这个验证的成本。所以这个功能开关在Release构建中应该是关闭的

参见xamarin-android#6727xamarin-macios#14130,了解关于这个改进的细节。

缓慢地加载ConfigurationManager

System.Configuration.ConfigurationManager 并不被很多移动应用程序所使用,而且事实证明,创建一个配置管理器是相当昂贵的!(例如,在Android上~7.59ms)。

在.NET MAUI中,有一个ConfigurationManager 在启动时被默认创建,我们可以使用Lazy<T> 推迟它的创建,所以除非请求,否则它不会被创建。

有关这一改进的细节,请参见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:<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在Release 构建的.NET Podcast样本中选择的顶级JIT时间。这些似乎是开发人员希望在.NET MAUI应用程序中使用的常用API。

为了确保这些方法在AOT配置文件中,我们在dotnet/maui中使用的 "记录应用程序 "中使用了这些API:

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

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

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

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

这导致了对配置文件的进一步补充:

var split = "foo;bar".Split(';');
var x = int.Parse("999");
x.ToString();

我们为Color.Parse(),Connectivity.NetworkAccess,DeviceInfo.Idiom, 和AppInfo.RequestedTheme 做了类似的改变,这些都是在.NET MAUI应用程序中常用的。

关于这些改进的细节,请参见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-loadmono_opt_aot_lazy_assembly_load设置,Android工作负载可以选择进入。我们发现这使Pixel 6 Pro上的dotnet new maui项目的启动速度提高了约25ms。

这在默认情况下是启用的,但如果需要,你可以通过.csproj ,禁用这个设置:

<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>

参见dotnet/runtime#67024xamarin-android#6940了解这些改进的细节。

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

dotnet trace 一个MAUI应用程序的输出显示,在第一次使用 APIs时,System.Uri加载UTF32和Latin1编码的时间大约为7ms。

namespace System
{
    internal static class UriHelper
    {
        internal static readonly Encoding s_noFallbackCharUTF8 = Encoding.GetEncoding(
            Encoding.UTF8.CodePage, new EncoderReplacementFallback(""), new DecoderReplacementFallback(""));

这个字段被意外地留在了这里。简单地删除s_noFallbackCharUTF8 字段,可以改善任何使用System.Uri 或相关API的.NET应用程序的启动。

有关这一改进的细节,请参见dotnet/runtime#65326

应用程序大小的改进

修复MauiImage尺寸的默认值

dotnet new maui 模板显示一个友好的".NET bot "图像。这是通过使用一个.svg 文件作为MauiImage 的内容实现的。

<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- everything else -->

.svg 默认情况下,MauiImage 使用widthheight 中的值作为图像的 "基本尺寸"。查看构建输出显示这些图像被缩放到:

objReleasenet6.0-androidresizetizerrmipmap-xxxhdpi
    appiconfg.png = 1824x1824
    dotnet_bot.png = 1676x2076

这对安卓设备来说似乎有点过大?我们可以简单地在模板中指定%(BaseSize) ,其中也给出了一个例子,说明如何为这些图片选择一个合适的尺寸:

<!-- Splash Screen -->
<MauiSplashScreen Include="Resourcesappiconfg.svg" Color="#512BD4" BaseSize="128,128" />

<!-- Images -->
<MauiImage Include="ResourcesImages*" />
<MauiImage Update="ResourcesImagesdotnet_bot.svg" BaseSize="168,208" />

这导致了更合适的尺寸:

objReleasenet6.0-androidresizetizerrmipmap-xxxhdpi
    appiconfg.png = 512x512
    dotnet_bot.png = 672x832

我们也可以修改.svg 的内容,但这可能不可取,这取决于图形设计师在其他设计工具中如何使用这个图片:

在另一个例子中,一个3008×5340的.jpg 图像:

<MauiImage Include="ResourcesImageslarge.jpg" />

...被放大到21360×12032!设置Resize="false" ,可以防止图像被调整大小,但我们将此作为非矢量图像的默认选项。今后,开发人员应该能够依靠默认值或根据需要指定%(BaseSize)%(Resize)

这些变化改善了启动性能以及应用程序的大小。有关这些改进的细节,请参见dotnet/maui#4759dotnet/maui#6419

删除Application.Properties和DataContractSerializer

Xamarin.Forms有一个通过Application.Properties 字典来持久化键值对的API。这在内部使用了DataContractSerializer ,这对于自成一体的移动应用程序来说并不是最好的选择。来自BCL的System.Xml 部分可能相当大,我们不希望在每个.NET MAUI应用程序中支付这一费用。

简单地去掉这个API和所有对DataContractSerializer的使用,在Android上就有了855KB的改进,在iOS上则有1MB的改进。

有关这一改进的细节,请参见dotnet/maui#4976

修剪未使用的HTTP实现

链接器开关,System.Net.Http.UseNativeHttpHandler 没有适当地修剪底层管理的HTTP处理器(SocketsHttpHandler )。默认情况下,AndroidMessageHandlerNSUrlSessionHandler 是用来利用底层的 Android 和 iOS 网络堆栈的。

通过解决这个问题,在任何.NET MAUI应用程序中,更多的IL代码能够被修剪掉。在一个例子中,一个使用HTTP的安卓应用能够完全修剪掉几个汇编:

  • 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#64852xamarin-android#6749xamarin-macios#14297,以了解有关这一改进的细节。

.NET播客样本的改进

我们对样本本身做了一些调整,其中的变化被认为是 "最佳实践"。

删除Microsoft.Extensions.Http的使用

使用Microsoft.Extensions.Http对于移动应用程序来说太沉重了,在这种情况下并没有提供任何实际价值。

因此,不要使用DI来实现HttpClient

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#66863dotnet-podcasts#44以了解有关这一改进的细节。

删除Newtonsoft.Json的使用

.NET播客样本使用了一个名为MonkeyCache的库,它依赖于Newtonsoft.Json。这本身并不是一个问题,只是.NET MAUI + Blazor应用程序依赖于一些ASP.NET Core库,而这些库又依赖于System.Text.Json。该应用程序实际上是为JSON解析库 "支付了两次费用",这对应用程序的大小产生了影响。

我们将MonkeyCache 2.0 移植到使用System.Text.Json,消除了应用程序中对Newtonsoft.Json 的需要。这使得iOS上的应用大小从29.3MB减少到26.1MB!

有关这一改进的细节,请参见monkey-cache#109dotnet-podcasts#58

在后台运行第一个网络请求

回顾dotnet trace 的输出,ShowsService中的初始请求阻塞了初始化Connectivity.NetworkAccessBarrel.Current.GetHttpClient 的UI线程。这项工作可以在后台线程中完成--在这种情况下会导致更快的启动时间。在Task.Run() 中包裹第一个调用,可以合理地提高这个样本的启动速度。

在Pixel 5a设备上平均运行10次:

Before
Average(ms): 843.7
Average(ms): 847.8
After
Average(ms): 817.2
Average(ms): 812.8

这种类型的变化,总是建议根据dotnet trace 或其他分析结果来决定,并测量前后的变化。

有关这一改进的细节,请参见dotnet-podcasts#57

实验性或高级选项

如果你想在Android上进一步优化你的.NET MAUI应用程序,有几个功能是高级或实验性的,默认情况下不启用。

修剪Resource.designer.cs

自Xamarin开始,Android应用程序包括一个生成的Properties/Resource.designer.cs 文件,用于访问AndroidResource 文件的整数标识符。这是一个C#/管理版本的R.java 类,以允许使用这些标识符作为普通的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、Google Play Services等,真的开始变得复杂了。以dotnet/maui#2606为例,在启动时有21,497个字段被设置!我们当时创造了一种方法来解决这个问题,但我们也有一个新的自定义修剪步骤,在构建时(修剪时)而不是在运行时执行修复。

要选择进入该功能:

<AndroidLinkResources>true</AndroidLinkResources>

这将使你的Release 构建时替换掉类似的情况:

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

取而代之的是直接内联整数:

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

这个功能的一个已知问题是Styleable 的值,如:

public partial class Styleable
{
    public static int[] ActionBarLayout = new int[] { 16842931 };
}

替换int[] 值目前不被支持,这使得它不是我们可以默认启用的。一些应用程序将能够打开这个功能,dotnet new maui 模板,也许许多.NET MAUI Android应用程序不会遇到这个限制。

在未来的.NET版本中,我们可能会默认启用$(AndroidLinkResources) ,也可能完全重新设计。

参见xamarin-android#5317xamarin-android#6696dotnet/maui#4912,以了解关于该功能的细节。

R8 Java代码缩减器

R8是整个程序的优化、缩减和最小化工具,它将java字节代码转换为优化的dex代码。R8 ,使用Proguard keep规则格式来指定一个应用程序的入口点。正如你所期望的,许多应用程序需要额外的Proguard 规则来保持工作。R8 可能过于激进,会删除被Java反射调用的东西,等等。我们还没有一个很好的方法来使这成为所有.NET Android应用程序的默认值。

要选择在Release 构建中使用R8 ,请在你的.csproj 中添加以下内容:

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

如果在启用这个功能后,启动你的应用程序的Release 构建时出现崩溃,请查看 adb logcat输出,看看哪里出了问题。

如果你看到一个java.lang.ClassNotFoundExceptionjava.lang.MethodNotFoundException ,你可能需要在你的项目中添加一个ProguardConfiguration 文件,如:

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

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

详情请见我们关于D8/R8的文档

AOT的一切

剖析的AOT是默认的,因为它在应用程序大小和启动性能之间提供了最好的权衡。如果应用程序的大小对你的应用程序来说不是一个问题,你可以考虑对所有的.NET程序集使用AOT。

要选择这一点,请在你的.csproj ,为Release配置添加以下内容:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <RunAOTCompilation>true</RunAOTCompilation>
  <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
</PropertyGroup>

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

AOT和LLVM

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

要选择在Release 构建中使用LLVM,请在你的.csproj 中添加以下内容:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <RunAOTCompilation>true</RunAOTCompilation>
  <EnableLLVM>true</EnableLLVM>
</PropertyGroup>

这个功能可以和Profiled AOT(或AOT-ing everything)结合使用。比较你的应用程序之前和之后,以了解EnableLLVM 对你的应用程序大小和启动性能有什么影响。

目前,需要安装Android NDK来使用这个功能。如果我们能解决这个要求,EnableLLVM 可能会成为未来.NET版本的默认功能。

详情请见我们关于EnableLLVM 的文档

记录一个自定义的AOT配置文件

默认情况下,Profiled AOT使用我们在.NET MAUI和Android工作负载中提供的 "内置 "配置文件,对大多数应用程序都有用。为了获得最佳的启动性能,你最好是记录一个特定于你的应用程序的配置文件。我们有一个实验性的Mono.Profiler.Android包用于这种情况。

要记录一个配置文件:

dotnet add package Mono.AotProfiler.Android --prerelease
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版本中完全支持记录自定义配置文件。

总结

我希望你喜欢我们的.NET MAUI性能论述。如果你能走到这一步,你当然应该拍拍自己的肩膀。

请尝试一下.NET MAUI,提交问题,或在dot.net/maui,了解更多信息!