.NET 多平台应用程序 UI (MAUI) 将 Android、iOS、macOS 和 Windows API 统一到单个 API 中,因此您可以编写一个在许多平台上本机运行的应用程序。我们专注于提高您的日常生产力以及应用程序的性能。我们认为,开发人员生产力的提高不应以牺牲应用程序性能为代价。
对于应用程序大小也可以这样说 — 空白的 .NET MAUI 应用程序中存在多少开销?当我们开始优化.NET MAUI时,很明显iOS需要一些工作来改善应用程序的大小,而Android缺乏启动性能。
项目的iOS应用程序大小最初约为18MB。同样,在早期预览版中,Android上的.NET MAUI启动时间看起来也不太好:dotnet new maui
应用 | 框架 | 启动时间(毫秒) |
---|---|---|
Xamarin.Android | 沙马林 | 306.5 |
Xamarin.Forms | 沙马林 | 498.6 |
Xamarin.Forms (Shell) | 沙马林 | 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次。
我们的目标是让 .NET MAUI 比其前身 Xamarin.Forms 更快,很明显,我们在 .NET MAUI 本身中还有一些工作要做。该模板已经比Xamarin.Android更快地启动,这主要是由于.NET 6中新的BCL和Mono运行时。dotnet new android
该模板尚未使用 Shell 导航模式,但正在计划将其作为 .NET MAUI 中的默认导航模式。我们知道,当我们采用此更改时,模板中会对性能造成影响。dotnet new maui
为了达到我们今天的位置,这是几个不同团队的合作。我们改进了 Microsoft.Extensions 和 DependencyInjection 用法、AOT 编译、Java 互操作、XAML、一般 .NET MAUI 中的代码等等。
尘埃落定后,我们到达了一个更好的地方:
应用 | 框架 | 启动时间(毫秒) |
---|---|---|
Xamarin.Android | 沙马林 | 306.5 |
Xamarin.Forms | 沙马林 | 498.6 |
Xamarin.Forms (Shell) | 沙马林 | 817.7 |
dotnet new android | .NET 6 (茂宜岛 GA) | 182.8 |
dotnet new maui (No Shell**) | .NET 6 (茂宜岛 GA) | 464.2 |
dotnet new maui (Shell) | .NET 6 (茂宜岛 GA) | 568.1 |
.NET Podcast App (Shell) | .NET 6 (茂宜岛 GA) | 814.2 |
**
– 这是不使用 Shell 的原始模板。dotnet new maui
详情如下,请尽情享受!
目录
启动性能改进
- 在移动设备上进行分析
- 随时间测量
- 剖面 AOT
- 单文件程序集存储
- Spanify RegisterNativeMembers
- 系统.反射.发出和构造函数
- 系统.反射.发射和方法
- 较新的 Java.Interop API
- 多维 Java 数组
- 将 Glide 用于安卓图像
- 减少 Java 互操作调用
- 将 Android XML 移植到 Java
- Remove Microsoft.Extensions.Hosting
- 启动时更少的 Shell 初始化
- 字体不应使用临时文件
- 编译时在平台上计算
- 在 XAML 中使用已编译的转换器
- 优化颜色解析
- 不要使用区域性感知字符串比较
- 懒惰地创建记录器
- 使用工厂方法进行依赖关系注入
- 加载配置懒惰地管理器
- 默认验证依赖性注入打开一般服务可伸缩性
- 改进内置 AOT 配置文件
- 启用 AOT 图像的延迟加载
- 删除系统中未使用的编码对象。Uri
应用大小改进
- 修复毛伊岛图像大小的默认值
- 删除应用程序.属性和数据协同串行器
- 修剪未使用的 HTTP 实现
.NET Podcast示例中的改进
- Remove Microsoft.Extensions.Http Usage
- Remove Newtonsoft.Json Usage
- 在后台运行第一个网络请求
实验性或高级选项
- 修剪资源.设计器.cs
- R8 Java Code Shrinker
- 奥特一切
- AOT 和 LLVM
- 记录自定义 AOT 配置文件
启动性能改进
在移动设备上进行分析
我不得不提到可用于移动平台的.NET诊断工具,因为它是我们使.NET MAUI更快的步骤0。
分析 .NET 6 Android 应用程序需要使用一种名为 dotnet-dsrouter 的工具
。此工具可以连接到Android,iOS等上运行的移动应用程序。这可能是我们用于分析 .NET MAUI 的最有影响力的工具。dotnet trace
要开始使用 和 ,请首先通过配置一些设置并启动: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 trace
dotnet-trace collect --diagnostic-port /tmp/maui-app --format speedscope
启动使用 和 构建的 Android 应用后,在输出时您会注意到连接:-c Release
-p:AndroidEnableProfiler=true
dotnet trace
Press <Enter> or <Ctrl+C> to exit...812 (KB)
只需在应用程序完全启动后按 Enter 键,即可将一个保存在当前目录中。您可以在 https://speedscope.app 打开此文件,以深入了解每种方法在应用程序启动期间花费的时间:*.speedscope
我建议在物理 Android 设备上进行分析构建,以获得最佳应用实际性能的视图。dotnet trace
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应用随着时间的推移所取得的进展:
这个图表是我们真正的重点,因为它是一个“真正的应用程序”,接近开发人员在自己的移动应用程序中看到的内容。
至于应用程序大小,这是一个更稳定的数字 - 当事情变得更糟或更好时,很容易归零:
剖面 AOT
在 .NET MAUI 的初始性能测试中,我们看到了 JIT(及时)与 AOT(提前)编译代码如何执行:
应用 | JIT 时间(毫秒) | AOT 时间(毫秒) |
---|---|---|
dotnet new maui | 1078.0毫秒 | 683.9毫秒 |
首次调用每个 C# 方法时会发生 JIT-ing,这会隐式影响移动应用程序中的启动性能。
同样有问题的是AOT引起的应用程序大小增加。Android 本机库将添加到每个 .NET 程序集的最终应用中。为了两全其美,启动跟踪或 Profiled AOT 是 Xamarin.Android 的当前功能。这是一种用于AOT应用程序启动路径的机制,只需适度增加应用程序大小,即可显着缩短启动时间。
将此作为 .NET 6 中生成的默认选项是完全有意义的。过去,Android NDK是使用Xamarin.Android进行任何类型的AOT所必需的(数千兆字节的下载)。我们在没有安装 Android NDK 的情况下完成了构建 AOT 应用程序的工作,使其成为未来的默认应用程序。Release
我们记录了 、和模板的内置配置文件,这些配置文件使大多数应用程序受益。如果您想在 .NET 6 中录制自定义配置文件,可以尝试我们的实验性 Mono.Profiler.Android 软件包。我们正在努力全面支持在未来的 .NET 版本中记录自定义配置文件。dotnet new androidmauimaui-blazor
单文件程序集存储
以前,如果您在自己喜欢的 zip 文件实用工具中查看了 Android 内容,则可以看到位于以下位置的 .NET 程序集:Release
.apk
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
两次:一次用于 ,另一次用于特定于体系结构的 blob。这对具有许多 .NET 程序集的应用程序产生了明显的影响。assemblies.blob
如果您需要从已编译的 Android 应用程序中检查这些程序集的 IL,我们创建了一个程序集-存储-读取器工具,用于“解压缩”这些文件。
另一种选择是在禁用以下设置的情况下构建应用程序:
dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:AndroidEnableAssemblyCompression=false
这使您可以使用自己喜欢的zip实用程序解压缩结果,并使用ILSpy等工具检查.NET程序集。这是诊断修剪器/链接器问题的好方法。.apk
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);
}
的列表是在托管 C# 代码中重写的 Java 本机接口 (JNI) 签名的和以 -分隔符的列表。对于在 C# 中重写的每个 Java 方法,您将获得其中之一。methods\n:
当实际的Java方法被调用给Android时:onCreate()
Activity
public void onCreate (android.os.Bundle p0)
{
n_onCreate (p0);
}
private native void n_onCreate (android.os.Bundle p0);
通过各种魔术和挥手,调用 Mono 运行时并在 C# 中调用我们的方法。n_onCreate
OnCreate()
拆分和分隔方法列表的代码是在 Xamarin 的早期使用 编写的。可以说Span<T>
当时还不存在,但我们现在可以使用它了!这提高了任何对 Java 类进行子类的 C# 类的成本,因此它比 .NET MAUI 更广泛。\n:string.Split()
你可能会问,“为什么要使用字符串?使用 Java 数组似乎比分隔字符串对性能的影响更差。在我们的测试中,调用 JNI 来获取 Java 数组元素,其性能比我们的新用法更差。对于如何在将来的 .NET 版本中重新构建它,我们有一些想法。string.Split
Span
除了 .NET 6 之外,此更改还附带在最新版本的 Xamarin.Android 中,面向当前客户。
系统.反射.发出和构造函数
从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 []>));
我们调用返回的委托,使得 是子类的,并且是该特定 C# 构造函数的任何参数。System.Reflection.Emit 在启动时首次使用它以及将来的每次调用都会产生巨大的成本。IntPtr
Handle
Java.Lang.Object
object[]
经过一些仔细的审查,我们可以创建字段,并将此代码简化为: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);
此代码的作用是在不调用构造函数的情况下创建一个对象(好怪吗?),设置字段,然后调用构造函数。这样做是为了使 C# 构造函数开始时的任何值都有效。构造函数内的任何 Java 互操作(如调用类上的其他 Java 方法)以及调用任何基本的 Java 构造函数都需要 。handle
Handle
Java.Lang.Object
Handle
新代码显著改进了从 Java 调用的任何 C# 构造函数,因此此特定更改不仅改进了 .NET MAUI。除了 .NET 6 之外,此更改还附带在最新版本的 Xamarin.Android 中,面向当前客户。
系统.反射.发射和方法
在 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;
}
例如,如果在 中未处理托管异常,则实际上最终会导致本机崩溃(并且没有托管的 C# 堆栈跟踪)。我们需要确保调试器可以在异常(如果附加了异常)时中断,否则将记录 C# 堆栈跟踪。OnCreate()
自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);
对于一个应用,此代码被调用两次,但对于一个应用,此代码调用了约 58 次!dotnet new android
dotnet new maui
相反,使用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 大小增长。我们正在研究如何在将来的 .NET 版本中改进这一点。
较新的 Java.Interop API
中的原始 Xamarin API 是以下 API:Java.Interop.dll
JNIEnv.CallStaticObjectMethod
其中,调用 Java 的“新方法”每次调用的内存分配更少:
JniEnvironment.StaticMethods.CallStaticObjectMethod
在生成时为 Java 方法生成 C# 绑定时,默认情况下会使用更新/较快的方法,并且在 Xamarin.Android 中已经存在了一段时间。以前,Java 绑定项目可以将 $(AndroidCodegenTarget)
设置为 ,这会在每次调用时缓存和重用实例。XAJavaInterop1jmethodID
这是一个问题的其余地方,是我们有“手动”绑定的任何地方。这些往往也是常用的方法,因此修复这些方法是值得的!
改善这种情况的一些示例:
JNIEnv.FindClass()
在 https://github.com/xamarin/xamarin-android/pull/6805中JavaList
和在 https://github.com/xamarin/xamarin-android/pull/6812中JavaList<T>
多维 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 类。
项目使用的一个特定的Android API是有问题的:dotnet new maui
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 版本中进一步改进这一点。一个这样的例子是https://github.com/dotnet/maui/pull/5654,我们只是在研究完全用Java创建数组。
将 Glide 用于安卓图像
Glide
是现代 Android 应用程序推荐的图像加载库。Google文档甚至建议使用它,因为内置的Android类可能很难正确使用。格列德克斯。形式
是在 Xamarin.Forms 中使用 Glide 的原型,但我们将 Glide 提升为在 .NET MAUI 中加载图像的“方式”。Bitmap
为了减少 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);
}
where 在 C# 中进行子类化,以处理托管代码中的完成。结果是,与之前在 Xamarin.Forms 中获得的性能相比,Web 图像的性能应得到显著提高。ImageLoaderCallback
减少 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);
}
因此,调用这两个方法将两次,p/invoke 将两次。创建一个小的Java包装器会更高性能,例如:stackalloc
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,则可以创建一个~18参数方法,用于创建。后续的属性更改可以直接调用标准的 Android API。View
View
这大大提高了性能,即使是非常简单的 .NET MAUI 控件:
方法 | 意味 着 | 错误 | 标准开发 | 第 0 代 | 分配 |
---|---|---|---|---|---|
边框(之前) | 323.2 微秒 | 0.82 微秒 | 0.68 微秒 | 0.9766 | 5 千字节 |
边框(之后) | 242.3 微秒 | 1.34 微秒 | 1.25 微秒 | 0.9766 | 5 千字节 |
内容视图(之前) | 354.6 微秒 | 2.61 微秒 | 2.31 微秒 | 1.4648 | 6 千字节 |
内容视图(之后) | 258.3 微秒 | 0.49 微秒 | 0.43 微秒 | 1.4648 | 6 千字节 |
将 Android XML 移植到 Java
查看Android上的输出,我们可以看到在以下方面花费的合理时间:dotnet trace
20.32.ms mono.android!Android.Views.LayoutInflater.Inflate
回顾堆栈跟踪,实际上时间花在Android / Java中以膨胀布局,并且.NET端没有发生任何工作。
如果你看看编译好的Android和Android Studio,你会发现XML只是普通的XML。只有少数标识符被转换为整数。这意味着Android必须解析这个XML并通过Java的反射API创建Java对象 - 似乎我们可以通过不使用XML来获得更快的性能?.apk
res/layouts/bottomtablayout.axml
测试标准的 BenchmarkDotNet 比较,我们发现,在涉及互操作时,Android 布局的使用甚至比 C# 更差:
方法 | 意味 着 | 错误 | 标准开发 | 分配 |
---|---|---|---|---|
爪哇岛 | 338.4 微秒 | 4.21 微秒 | 3.52 微秒 | 744 字节 |
夏普 | 410.2 微秒 | 7.92 微秒 | 6.61 微秒 | 1,336 字节 |
XML | 490.0 微秒 | 7.77 微秒 | 7.27 微秒 | 2,321 字节 |
接下来,我们将 BenchmarkDotNet 配置为执行单次运行,以更好地模拟启动时会发生什么:
方法 | 意味 着 |
---|---|
爪哇岛 | 4.619 毫秒 |
夏普 | 37.337 毫秒 |
XML | 39.364 毫秒 |
我们查看了 .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操作系统跳过加载和解析以“膨胀”Java对象。我们在整个dotnet/maui中都带着这个想法,删除了启动时的所有调用。.xml
LayoutInflater.Inflate()
Remove Microsoft.Extensions.Hosting
Microsoft.Extensions.Hosting
提供了一个 .NET 通用主机,用于管理 .NET 应用程序中的依赖关系注入、日志记录、配置和应用程序生命周期。这对启动时间产生了影响,似乎不适合移动应用程序。
从 .NET MAUI 中删除用法是有意义的。.NET MAUI 不是尝试与“通用主机”互操作来构建 DI 容器,而是具有自己的简单实现,该实现针对移动启动进行了优化。此外,默认情况下,.NET MAUI 不再添加日志记录提供程序。Microsoft.Extensions.Hosting
通过这一变化,我们看到Android应用程序的启动时间缩短了5-10%。它将iOS上同一应用程序的大小从 = > 。dotnet new maui
19.2 MB
18.0 MB
启动时更少的 Shell 初始化
Xamarin.Forms Shell 是跨平台应用程序中的导航模式。此模式被引入到 .NET MAUI,建议将其作为构建应用程序的默认方式。
当我们发现在启动时使用Shell的成本(对于Xamarin.Forms和.NET MAUI)时,我们发现了几个需要优化的地方:
- 不要在启动时解析路由 - 等到出现需要它们的导航。
- 如果没有为导航提供查询字符串,则只需跳过处理查询字符串的代码。这将删除大量使用 System.Reflection 的代码路径。
- 如果页面没有可见 ,则不要设置菜单项或任何外观元素。
BottomNavigationView
字体不应使用临时文件
在 .NET MAUI 应用程序加载字体方面花费了大量时间:
32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3<string, Microsoft.Maui.FontWeight, bool>)
在审查代码时,它正在做比需要的更多的工作:
- 将文件保存到临时文件夹。
AndroidAsset
- 使用 Android API 加载文件。
Typeface.CreateFromFile()
我们实际上可以直接使用Android API,而根本不使用临时文件。Typeface.CreateFromAsset()
编译时在平台上计算
标记扩展的用法:{OnPlatform}
<Label Text="Platform: " />
<Label Text="{OnPlatform Default=Unknown, Android=Android, iOS=iOS" />
...实际上可以在编译时计算,其中 和 得到适当的值。在将来的 .NET 版本中,我们将研究 XML 元素的相同优化。net6.0-android
net6.0-ios
<OnPlatform/>
在 XAML 中使用已编译的转换器
以下类型现在在 XAML 编译时转换,而不是在运行时转换:
Color
: https://github.com/dotnet/maui/pull/4687CornerRadius
:https://github.com/dotnet/maui/pull/5192FontSize
: https://github.com/dotnet/maui/pull/5338GridLength
:https://github.com/dotnet/maui/pull/5489
RowDefinitionColumnDefinition
这导致从文件更好/更快地生成 IL。.xaml
优化颜色解析
可以重写 的原始代码,以更好地利用和避免字符串分配。Microsoft.Maui.Graphics.Color.Parse()
Span<T>
方法 | 意味 着 | 错误 | 标准开发 | 第 0 代 | 分配 |
---|---|---|---|---|---|
解析(之前) | 99.13 ns | 0.281 ns | 0.235 ns | 0.0267 | 168 字节 |
解析(之后) | 52.54 ns | 0.292 ns | 0.259 ns | 0.0051 | 32 字节 |
能够在 dotnet/csharplang#1881 上使用 -statement 将在将来的 .NET 版本中进一步改善这种情况。switchReadonlySpan<char>
不要使用区域性感知字符串比较
回顾一个项目的输出,显示了Android上第一个文化感知字符串比较的实际成本:dotnet trace
dotnet new maui
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
懒惰地创建记录器
API在启动上花费了一些时间,做一些可以推迟到以后的工作。我们还可以改进 Microsoft.Extensions 中日志记录基础结构的一般用法。ConfigureFonts()
我们所做的一些改进是:
- 推迟创建“记录器”类,直到需要它们。
- 默认情况下,内置日志记录基础结构处于禁用状态,并且必须显式启用。
- 延迟在Android中呼叫,直到需要它。
Path.GetTempPath()
EmbeddedFontLoader
- 不要用于创建通用记录器。而是直接获取服务,以便对其进行缓存。
ILoggerFactory
ILogger
使用工厂方法进行依赖关系注入
使用 时,注册服务,例如:Microsoft.Extensions.DependencyInjection
IServiceCollection services /* ... */;
services.TryAddSingleton<IFooService, FooService>();
Microsoft.Extensions 必须执行一些 System.Reflection 才能创建 的第一个实例。这在Android上的输出中很明显。FooService
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/匿名方法,而不涉及 System.Reflection。
我们在所有dotnet/maui上都进行了此改进,并利用了BanbanEdApiAnalyzers
,这样就不会有人意外地使用较慢的过载。TryAddSingleton()
默认验证依赖性注入打开一般服务可伸缩性
.NET Podcast示例在以下方面花费了 4-7 毫秒的时间:
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()
MSBuild 属性触发此方法运行。此功能开关可确保正确应用于打开依赖关系注入中使用的泛型类型。$(VerifyDependencyInjectionOpenGenericServiceTrimmability)
DynamicallyAccessedMembers
在基本 .NET SDK 中,当 .但是,Android 应用程序不会在内部版本中进行设置,因此开发人员会错过此验证。PublishTrimmed=true
PublishTrimmed=true
Debug
相反,在已发布的应用中,我们不希望支付执行此验证的成本。因此,此功能开关在生成中应处于关闭状态。Release
加载配置懒惰地管理器
System.Configuration.ConfigurationManager
许多移动应用程序都没有使用,而且创建一个移动应用程序的成本非常高!(例如,在安卓设备上约为7.59毫秒)
在 .NET MAUI 中,默认情况下会创建一个,我们可以使用 延迟其创建,因此除非请求,否则不会创建它。ConfigurationManager
Lazy<T>
改进内置 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。Release
为了确保这些方法在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();
我们对 、 、 、 做了类似的更改,这应该在 .NET MAUI 应用程序中常用。Color.Parse()
Connectivity.NetworkAccess
DeviceInfo.Idiom
AppInfo.RequestedTheme
如果您想在.NET 6中录制自定义AOT配置文件,可以尝试我们的实验性Mono.Profiler.Android软件包。我们正在努力全面支持在未来的 .NET 版本中记录自定义配置文件。
启用 AOT 图像的延迟加载
以前,Mono 运行时会在启动时加载所有 AOT 映像,以验证托管 .NET 程序集(如 )的 MVID 是否与 AOT 映像 () 匹配。在大多数 .NET 应用程序中,某些 AOT 映像可能不需要在以后加载。Foo.dll
libFoo.dll.so
Mono 中引入了一个新的设置或设置,Android 工作负载可以选择加入该设置。我们发现,这在Pixel 6 Pro上的项目启动速度提高了约25毫秒。--aot-lazy-assembly-load
mono_opt_aot_lazy_assembly_load
dotnet new maui
默认情况下启用此功能,但如果需要,您可以在 via 中禁用此设置:.csproj
<AndroidAotEnableLazyLoad>false</AndroidAotEnableLazyLoad>
删除系统中未使用的编码对象。Uri
dotnet trace
MAUI 应用程序的输出显示,首次使用 API 时,加载 UTF32 和 Latin1 编码花费了大约 7 毫秒:System.Uri
namespace System
{
internal static class UriHelper
{
internal static readonly Encoding s_noFallbackCharUTF8 = Encoding.GetEncoding(
Encoding.UTF8.CodePage, new EncoderReplacementFallback(""), new DecoderReplacementFallback(""));
此字段被意外保留。只需删除该字段,即可改进使用或相关 API 的任何 .NET 应用程序的启动。s_noFallbackCharUTF8
System.Uri
应用大小改进
修复毛伊岛图像大小的默认值
该模板显示一个友好的“.NET 机器人”图像。这是通过使用包含以下内容的文件作为 来实现的: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
width
height
.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"
%(BaseSize)
%(Resize)
这些更改改进了启动性能和应用大小。
删除应用程序.属性和数据协同串行器
Xamarin.Forms有一个API,用于通过字典持久化键值对。这在内部使用,对于独立和修剪的移动应用程序来说,这不是最佳选择。BCL 的部分内容可能相当大,我们不想在每个 .NET MAUI 应用程序中为此成本付费。Application.Properties
DataContractSerializer
System.Xml
简单地删除此API和所有使用,就可以在Android上实现约855KB的改进,并在iOS上实现约1MB的改进。DataContractSerializer
修剪未使用的 HTTP 实现
链接器开关没有适当地修剪掉基础托管 HTTP 处理程序 ()。默认情况下,用于利用底层 Android 和 iOS 网络堆栈。System.Net.Http.UseNativeHttpHandler
SocketsHttpHandler
AndroidMessageHandler
NSUrlSessionHandler
通过修复此问题,可以在任何 .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
.NET Podcast示例中的改进
我们对样本本身进行了一些调整,其中更改被视为“最佳实践”。
Remove Microsoft.Extensions.Http Usage
使用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
Remove Newtonsoft.Json Usage
.NET Podcast示例使用的是一个名为MonkeyCache
的库,该库依赖于Newtonsoft.Json。这本身不是问题,除了.NET MAUI + Blazor应用程序依赖于一些 ASP.NET 核心库,而这些库又依赖于System.Text.Json。该应用程序实际上为JSON解析库“支付了两倍费用”,这对应用程序大小产生了影响。
我们将 2.0 移植为使用 System.Text.Json,无需在应用中使用。这将iOS上的应用程序大小从29.3MB减少到26.1MB!MonkeyCache
Newtonsoft.Json
在后台运行第一个网络请求
查看输出时,中的初始请求阻止了 UI 线程初始化 、 和 。这项工作可以在后台线程中完成 - 在这种情况下,启动时间更快。将第一个调用包装在 中可使此示例的启动时间提高一个合理的量。dotnet trace
ShowsService
Connectivity.NetworkAccess
Barrel.Current.Get
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 trace
实验性或高级选项
如果您想在Android上进一步优化.NET MAUI应用程序,则有几个功能是高级或实验性的,默认情况下不启用。
修剪资源.设计器.cs
自 Xamarin 开始以来,Android 应用程序包含一个生成的文件,用于访问文件的整数标识符。这是该类的 C#/托管版本,允许将这些标识符用作普通 C# 字段(有时),而无需与 Java 进行任何互操作。Properties/Resource.designer.cs
AndroidResource
R.java
const
在 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);
当你构建时,Android gradle插件实际上并没有把这个类放在包里。相反,消耗的Android应用程序实际上是知道整数是什么。因此,该类是在 Android 应用程序构建时生成的,为每个使用的 Android 库生成一个类。com.yourlibrary.aar
R
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;
}
}
这种情况已经运行了相当长的一段时间,但不幸的是,Google库中的资源数量(如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 };
}
当前不支持替换值,这使得默认情况下我们无法启用它。某些应用程序将能够启用此功能,模板以及许多.NET MAUI Android应用程序可能不会遇到此限制。int[]
dotnet new maui
在将来的 .NET 版本中,我们可能能够默认启用,或者完全重新设计内容。$(AndroidLinkResources)
R8 Java代码收缩器
R8是将java字节代码转换为优化dex代码的全程序优化,收缩和缩小工具。 使用 Keep 规则格式指定应用程序的入口点。正如您所料,许多应用程序需要其他规则来保持工作正常。 可能过于激进,并删除由Java反射等调用的内容。我们还没有一个好的方法来使它成为所有.NET Android应用程序的默认设置。R8ProguardProguardR8
要选择用于构建,请将以下内容添加到您的:R8
Release
.csproj
<!-- NOTE: not recommended for Debug builds! -->
<AndroidLinkTool Condition="'$(Configuration)' == 'Release'">r8</AndroidLinkTool>
如果启动应用程序版本在启用此功能后崩溃,请查看 adb logcat
输出以查看出了什么问题。Release
如果看到 或 ,则可能需要向项目中添加文件,例如:java.lang.ClassNotFoundException
java.lang.MethodNotFoundException
ProguardConfiguration
<ItemGroup>
<ProguardConfiguration Include="proguard.cfg" />
</ItemGroup>
-keep class com.thepackage.TheClassYouWantToPreserve { *; <init>(...); }
我们正在研究在将来的 .NET 版本中默认启用的选项。R8
奥特一切
分析的 AOT 是默认设置,因为它在应用大小和启动性能之间提供了最佳权衡。如果应用程序不关心应用大小,则可以考虑对所有 .NET 程序集使用 AOT。
要选择此选项,请将以下内容添加到您的配置中:.csproj
Release
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
</PropertyGroup>
这将减少在应用程序中启动期间发生的 JIT 编译量,以及导航到后面的屏幕等。
AOT 和 LLVM
LLVM提供了一个现代的源代码和目标无关的优化器,可以与Mono AOT编译器输出结合使用。结果是应用大小稍大,生成时间更长,运行时性能更好。Release
要选择使用 LLVM 进行构建,请将以下内容添加到您的:Release
.csproj
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<EnableLLVM>true</EnableLLVM>
</PropertyGroup>
此功能可以与“分析”AOT(或“AOT-ing 所有内容”)结合使用。比较您的应用程序之前和之后,以了解对应用程序大小和启动性能的影响。EnableLLVM
目前,需要安装 Android NDK 才能使用此功能。如果我们能解决这个要求,可能会成为未来.NET版本中的默认。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 版本中记录自定义配置文件。