关于microsoft:NET-MAUI-性能提升

60次阅读

共计 29318 个字符,预计需要花费 74 分钟才能阅读完成。

.NET 多平台应用程序 UI (MAUI)将 android、iOS、macOS 和 Windows API 对立为一个 API,这样你就能够编写一个应用程序在许多平台上本机运行。咱们专一于进步您的日常生产力以及您的应用程序的性能。咱们认为,开发人员生产率的进步不应该以应用程序性能为代价。

应用程序的大小也是如此——在一个空白的.NET MAUI 应用程序中存在什么开销? 当咱们开始优化.NET MAUI 时,很显著 iOS 须要做一些工作来改善应用程序的大小,而 android 则不足启动性能。

一个 dotnet new maui 我的项目的 iOS 应用程序最后大概是 18MB。同样,在之前的预览中.NET MAUI 在 android 上的启动工夫也不是很现实:

.NET Podcast:
https://github.com/microsoft/…

这是在 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 代码,等等方面。

.NET Podcast App (Shell):https://github.com/microsoft/…
** 这是原始的 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 输入时,你会留神到连贯:

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

在您的应用程序齐全启动后,只需按下 enter 键就能够失去一个保留在当前目录的 *.speedscope。你能够在 https://speedscope.app 上关上这个文件,深刻理解每个办法在应用程序启动期间所破费的工夫:

在 android 应用程序中应用 dotnet 跟踪的更多细节,请参阅咱们的文档。我倡议在 android 设施上剖析 Release 版本,以取得利用在事实世界中的最佳体现。

▌测量随着工夫的推移

咱们在.NET 根底团队的敌人建设了一个管道来跟踪.NET MAUI 性能场景,例如:

  • 包大小
  • 磁盘大小(未压缩)
  • 单个文件分类
  • 应用程序启动
    随着工夫的推移,这使咱们可能看到改良或回归的影响,看到 dotnet/maui 回购的每个提交的数字。咱们还能够确定这种差别是否是由 xamarin-android、xamarin-macios 或 dotnet/runtime 中的变动引起的。

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

留神,Pixel 4a 比 Pixel 5 要慢得多。

咱们能够准确地指出在 dotnet/maui 中产生的回归和改良。这对于追踪咱们的指标是十分有用的。

同样地,咱们能够在雷同的 Pixel 4a 设施上看到.NET Podcast 利用随着工夫的推移所获得的停顿:

这张图表是咱们真正关注的焦点,因为它是一款“真正的利用”,靠近于开发者在本人的手机利用中看到的内容。

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

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

▌异形 AOT

在咱们对.NET MAUI 的初始性能测试中,咱们看到了 JIT(及时)和 AOT(提前)编译的代码是如何执行的:

每次调用 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<T> 在那时还不存在,但咱们当初能够应用它! 这进步了任何继承 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<IntPtr, object?[]?>) method.CreateDelegate (typeof (Action <IntPtr, object []>));

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

通过认真的审查,咱们能够将 handle 字段设置为外部的,并将此代码简化为:

var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType);
if (newobj is Java.Lang.Object o) {o.handle = jobject;} else if (newobj is Java.Lang.Throwable throwable) {throwable.handle = jobject;} else {throw new InvalidOperationException ($"Unsupported type:'{newobj}'");
}
cinfo.Invoke (newobj, parms);

这段代码所做的是在不调用构造函数的状况下创立一个对象,设置句柄字段,而后调用构造函数。这样做是为了当 c# 构造函数开始时,Handle 在任何 Java.Lang.Object 上都是无效的。构造函数外部的任何 Java 互操作 (比方调用类上的其余 Java 办法) 以及调用任何根本 Java 构造函数都须要 Handle。
新代码显著改良了从 Java 调用的任何 c# 构造函数,因而这个非凡的更改改良的不仅仅是.NET MAUI。除了.NET 6 之外,针对以后客户 Xamarin. android 的最新版本也附带了这一更改。
查看 xamarin-android#6766 理解这个改良的详细信息。

▌System.Reflection.Emit 和办法

当你在 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 理解这个改良的详细信息。

▌更新的 Java.Interop APIs

Java.Interop.dll 中原始的 Xamarin api 是这样的 api:

  • JNIEnv.CallStaticObjectMethod

在 Java 中调用的“新办法”每次调用占用的内存更少:

  • JniEnvironment.StaticMethods.CallStaticObjectMethod

当在构建时为 Java 办法生成 c# 绑定时,默认应用更新 / 更快的办法—在 Xamarin.Android 中曾经有一段时间了。以前,Java 绑定我的项目能够将 $(AndroidCodegenTarget)设置为 XAJavaInterop1,它在每次调用中缓存和重用 jmethodID 实例。请参阅 java.interop 文档获取对于该个性的历史记录。

其余有问题的中央是有“手动”绑定的中央。这些往往也是常常应用的办法,所以值得修复这些!

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

  • JNIEnv.FindClass()在 xamarin-android#6805
  • JavaList 和 JavaList<T> 在 xamarin-android#6812

$(AndroidCodegenTarget):
https://docs.microsoft.com/en…
java.interop:
https://github.com/xamarin/Ja…
xamarin-android#6805:
https://github.com/xamarin/xa…
xamarin-android#6812:
https://github.com/xamarin/xa…

▌多维 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 理解这个改良的详细信息。

▌为 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。

▌缩小 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 控件来说,这在性能上有了显著的进步:

请参阅 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# 更差:

接下来,咱们将 BenchmarkDotNet 配置为单次运行,以更好地模仿启动时产生的状况:
办法

咱们在.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 理解这些改良的详细信息。

▌删除 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<string, Microsoft.Maui.FontWeight, bool>)

查看代码时,它所做的工作比须要的更多:
1. 将 androidAsset 文件保留到长期文件夹。
2. 应用 android API, Typeface.CreateFromFile()来加载文件。

咱们实际上能够间接应用 Typeface.CreateFromAsset() android API,基本不必临时文件。
请参阅 dotnet/maui#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。

▌在 XAML 中应用编译转换器

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

  • 色彩:dotnet /maui# 4687
  • 角半径: dotnet / maui # 5192
  • 字形大小:dotnet / maui # 5338
  • 网格长度, 行定义, 列定义: dotnet/maui#5489
    这导致从.xaml 文件生成更好 / 更快的 IL。

▌优化色彩解析

Microsoft.Maui.Graphics.Color.Parse()的原始代码能够重写,以更好地应用 Span 并防止字符串调配。


可能在 ReadonlySpan<char> dotnet/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<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 理解无关此改良的详细信息。

▌懈怠地负载 ConfigurationManager

configurationmanager 并没有被许多挪动应用程序应用,而且创立一个是十分低廉的!(例如,在 android 上约为 7.59ms)

在.NET MAUI 中,一个 ConfigurationManager 在启动时默认被创立,咱们能够应用 Lazy 提早它的创立,所以它将不会被创立,除非申请。

请参阅 dotnet/maui#5348 理解无关此改良的详细信息。

▌默认 VerifyDependencyInjectionOpenGenericServiceTrimmability

.NET Podcast 样本破费了 4 -7ms 的工夫:

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

MSBuild 属性 $(verifydependencyinjectionopengenericservicetrimability)触发该办法运行。这个个性开关确保 dynamallyaccessedmembers 被正确地利用于关上依赖注入中的泛型类型。

在根底.NET SDK 中,当 publishtrim =true 时,该开关将被启用。然而,android 应用程序在 Debug 版本中并没有设置 publishtrim =true,所以开发者错过了这个验证。

相同,在已公布的应用程序中,咱们不想领取这种验证的老本。所以这个个性开关应该在 Release 版本中敞开。

查看 xamarin-android#6727 和 xamarin-macios#14130 理解对于这个改良的详细信息。

▌改良内置 AOT 配置文件

Mono 运行时有一个对于每个办法的 JIT 工夫的报告(参见咱们的文档),例如:

Total(ms) | Self(ms) | Method
     3.51 |     3.51 | Microsoft.Maui.Layouts.GridLayoutManager/GridStructure:.ctor (Microsoft.Maui.IGridLayout,double,double)
     1.88 |     1.88 | Microsoft.Maui.Controls.Xaml.AppThemeBindingExtension/<>c__DisplayClass20_0:<Microsoft.Maui.Controls.Xaml.IMarkupExtension<Microsoft.Maui.Controls.BindingBase>.ProvideValue>g__minforetriever|0 ()
     1.66 |     1.66 | Microsoft.Maui.Controls.Xaml.OnIdiomExtension/<>c__DisplayClass32_0:<ProvideValue>g__minforetriever|0 ()
     1.54 |     1.54 | Microsoft.Maui.Converters.ThicknessTypeConverter:ConvertFrom (System.ComponentModel.ITypeDescriptorContext,System.Globalization.CultureInfo,object)

这是一个应用 Profiled AOT 的版本构建中.NET Podcast 示例中的顶级 jit 工夫抉择。这些仿佛是开发人员心愿在. net MAUI 应用程序中应用的罕用 api。
为了确保这些办法在 AOT 配置文件中,咱们在 dotnet/maui 中应用了这些 api

_=new Microsoft.Maui.Layouts.GridLayoutManager(new Grid()).Measure(100, 100);

<SolidColorBrush x:Key="ProfiledAot_AppThemeBinding_Color" Color="{AppThemeBinding Default=Black}"/>
<CollectionView x:Key="ProfiledAot_CollectionView_OnIdiom_Thickness" Margin="{OnIdiom Default=1,1,1,1}" />

在这个测试应用程序中调用这些办法能够确保它们位于内置的. net MAUI AOT 配置文件中。
在这个更改之后,咱们看了一个更新的 JIT 报告:

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

这导致了进一步的补充:

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

咱们对 Color.Parse()、Connectivity 做了相似的批改.NETworkAccess DeviceInfo。成语,AppInfo。.NET MAUI 应用程序中应该常常应用的 requestdtheme。

请参阅 dotnet/maui#5559, dotnet/maui#5682,和 dotnet/maui#6834 理解这些改良的详细信息。

如果你想在.NET 6 中记录一个自定义的 AOT 配置文件,你能够尝试咱们的试验包 Mono.Profiler.Android。咱们正在致力在将来的.NET 版本中齐全反对记录自定义概要文件。

▌启用 AOT 图像的提早加载

以前,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 理解这些改良的详细信息。

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

一个 MAUI 应用程序的 dotnet 跟踪输入,显示大概 7ms 破费了加载 UTF32 和 Latin1 编码的第一次零碎。应用 Uri api:

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

这个字段是不小心留在原地的。只需删除 s_noFallbackCharUTF8 字段,就能够改良任何应用 System.Uri 或相干的 api 的. net 应用程序的启动。

参见 dotnet/runtime#65326 理解无关此改良的详细信息。

应用程序大小的改良

▌修复默认的 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 理解这些改良的细节。

▌删除 Application.Properties 和 DataContractSerializer

Xamarin.Forms 有一个 API,用于通过 Application.Properties 字典长久化键值对。这在外部应用了 DataContractSerializer,这对于自蕴含和修剪的挪动应用程序不是最佳抉择。来自 BCL 的 System.Xml 的局部可能相当大,咱们不想在每个.NET MAUI 应用程序中都为此付出代价。

简略地删除这个 API 和所有 DataContractSerializer 的应用,在 android 上能够进步约 855KB,在 iOS 上进步约 1MB。

请参阅 dotnet/maui#4976 理解无关此改良的详细信息。

▌修剪未应用的 HTTP 实现

System.NET.Http.UseNativeHttpHandler 没有适当地削减底层托管 HTTP 处理程序 (SocketsHttpHandler)。默认状况下,androidMessageHandler 和 NSUrlSessionHandler 被用来利用底层的 android 和 iOS 网络栈。
通过修改这个问题,在任何.NET MAUI 应用程序中都能够删除更多的 IL 代码。在一个例子中,一个应用 HTTP 的 android 应用程序可能齐全删除几个程序集:

  • Microsoft.Win32.Primitives.dll
  • System.Formats.Asn1.dll
  • System.IO.Compression.Brotli.dll
  • System.NET.NameResolution.dll
  • System.NET.NETworkInformation.dll
  • System.NET.Quic.dll
  • System.NET.Security.dll
  • System.NET.Sockets.dll
  • System.Runtime.InteropServices.RuntimeInformation.dll
  • System.Runtime.Numerics.dll
  • System.Security.Cryptography.Encoding.dll
  • System.Security.Cryptography.X509Certificates.dll
  • System.Threading.Channels.dll

查看 dotnet/runtime#64852, xamarin-android#6749,和 xamarin-macios#14297 对于这个改良的详细信息。

.NET Podcast 示例中的改良

咱们对样本自身做了一些调整,其中更改被认为是“最佳实际”。

▌删除 Microsoft.Extensions.Http 用法

应用 Microsoft.Extensions.Http 对于挪动应用程序来说太重了,并且在这种状况下没有提供任何真正的价值。
因而,HttpClient 不应用 DI:

builder.Services.AddHttpClient<ShowsService>(client => 
{client.BaseAddress = new Uri(Config.APIUrl);
});
// Then in the service 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 理解无关改良的详细信息。

▌删除 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 理解无关改良的详细信息。

▌在后盾运行第一个网络申请

回顾 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 理解无关此改良的详细信息。

实验性或高级选项

如果你想在 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 理解该性能的详细信息。

▌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 文档。

▌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:

EnableLLVM 的文档:

▌记录自定义 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 版本中齐全反对记录自定义概要文件。

论断

我心愿您喜爱咱们的.NET MAUI 性能阐述。请尝试.NET MAUI 并且能够在 http://dot.net/maui 理解更多!



长按辨认二维码
关注微软中国 MSDN

点击获取学习资源~

正文完
 0