.NET MAUI 性能提升(下)

继上一篇文章为大家介绍了启动性能的优化,今天我们来看一看其他令人欣喜的性能提升。

主要内容 

❖ 应用程序大小的改进
  • 修复默认的MauiImage大小
  • 删除Application.Properties 和DataContractSerializer.
  • 修剪未使用的HTTP实现

❖ .NET Podcast示例中的改进
  • 删除Microsoft.Extensions.Http用法
  • 删除Newtonsoft.Json使用
  • 在后台运行第一个网络请求

❖ 实验性或高级选项
  • 修剪Resource.designer.cs
  • R8 Java代码收缩器
  • AOT一切
  • AOT和LLVM
  • 记录自定义AOT配置文件

应用程序大小的改进

▌修复默认的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了解这些改进的细节。
  • 修复默认的MauiImage大小:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#fix-defaults-for-mauiimage-sizes?ocid=AID3045631

  • dotnet/maui#4759:

    https://github.com/dotnet/maui/pull/4759

  • dotnet/maui#6419:

    https://github.com/dotnet/maui/pull/6419
▌删除Application.Properties 和DataContractSerializer
Xamarin.Forms 有一个 API,用于通过 Application.Properties 字典持久化键值对。这在内部使用了DataContractSerializer,这对于自包含和修剪的移动应用程序不是最佳选择。来自BCL的System.Xml的部分可能相当大,我们不想在每个.NET MAUI应用程序中都为此付出代价。
简单地删除这个API和所有DataContractSerializer的使用,在android上可以提高约855KB,在iOS上提高约1MB。
请参阅dotnet/maui#4976了解有关此改进的详细信息。
  • 删除Application.Properties 和DataContractSerializer:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-applicationproperties-and-datacontractserializer?ocid=AID3045631

  • dotnet/maui#4976:

    https://github.com/dotnet/maui/pull/4976

▌修剪未使用的HTTP实现

System.NET.Http.UseNativeHttpHandler没有适当地削减底层托管HTTP处理程序(SocketsHttpHandler)。默认情况下,androidMessageHandler和NSUrlSessionHandler被用来利用底层的android和iOS网络栈。

通过修正这个问题,在任何.NET MAUI应用程序中都可以删除更多的IL代码。在一个例子中,一个使用HTTP的android应用程序能够完全删除几个程序集:

  • Microsoft.Win32.Primitives.dll

  • System.Formats.Asn1.dll

  • System.IO.Compression.Brotli.dll

  • System.NET.NameResolution.dll

  • System.NET.NETworkInformation.dll

  • System.NET.Quic.dll

  • System.NET.Security.dll

  • System.NET.Sockets.dll

  • System.Runtime.InteropServices.RuntimeInformation.dll

  • System.Runtime.Numerics.dll

  • System.Security.Cryptography.Encoding.dll

  • System.Security.Cryptography.X509Certificates.dll

  • System.Threading.Channels.dll

查看dotnet/runtime#64852, xamarin-android#6749,和xamarin-macios#14297关于这个改进的详细信息。
  • 修剪未使用的HTTP实现:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trim-unused-http-implementations?ocid=AID3045631

  • dotnet/runtime#64852:

    https://github.com/dotnet/runtime/pull/64852

  • xamarin-android#6749:

    https://github.com/xamarin/xamarin-android/pull/6749

  • xamarin-macios#14297:

    https://github.com/xamarin/xamarin-macios/pull/14297

 

.NET Podcast示例中的改进

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

▌删除Microsoft.Extensions.Http用法

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

因此,HttpClient不使用DI:

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

  • .NET Podcast:

    https://github.com/microsoft/dotnet-podcasts

  • 删除Microsoft.Extensions.Http用法:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-microsoftextensionshttp-usage?ocid=AID3045631

  • dotnet/runtime#66863:

    https://github.com/dotnet/runtime/issues/66863

  • dotnet podcasts#44:

    https://github.com/microsoft/dotnet-podcasts/pull/44
▌删除Newtonsoft.Json使用

.NET Podcast 样本使用了一个名为MonkeyCache的库,它依赖于Newtonsoft.Json。这本身并不是一个问题,只是.NET MAUI + Blazor应用程序依赖于一些ASP.NET Core库反过来依赖于System.Text.Json。这款应用实际上是为JSON解析库“付了两倍钱”,这对应用的大小产生了影响。

我们移植了MonkeyCache 2.0来使用System.Text。Json,不需要Newtonsoft。这将iOS上的应用大小从29.3MB减少到26.1MB!
参见monkey-cache#109和dotnet-podcasts#58了解有关改进的详细信息。
  • 删除Newtonsoft.Json使用:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#remove-newtonsoftjson-usage?ocid=AID3045631

  • .NET Podcast :

    https://github.com/microsoft/dotnet-podcasts/pull/44

  • MonkeyCache:

    https://github.com/jamesmontemagno/monkey-cache

  • monkey-cache#109:

    https://github.com/jamesmontemagno/monkey-cache/pull/109

  • dotnet-podcasts#58:

    https://github.com/microsoft/dotnet-podcasts/pull/58
▌在后台运行第一个网络请求

回顾dotnet跟踪输出,初始请求在ShowsService阻塞UI线程初始化连接.NETworkAccess Barrel.Current。得到,HttpClient。这项工作可以在后台线程中完成-在这种情况下导致更快的启动时间。在Task.Run()中封装第一个调用,可以在一定程度上提高这个示例的启动效率。

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

BeforeAverage(ms): 843.7Average(ms): 847.8AfterAverage(ms): 817.2Average(ms): 812.8

对于这种类型的更改,总是建议根据dotnet跟踪或其他分析结果来做出决定,并度量更改前后的变化。

请参阅dotnet-podcasts#57了解有关此改进的详细信息。

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

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#run-first-network-request-in-background?ocid=AID3045631

  • dotnet-podcasts#57:
    https://github.com/microsoft/dotnet-podcasts/pull/57

实验性或高级选项 

如果你想在android上进一步优化你的.NET MAUI应用程序,这里有一些高级或实验性的特性,默认情况下不是启用的。

▌修剪Resource.designer.cs

自从Xamarin诞生以来,android应用程序就包含了一个生成的Properties/Resource.designer.cs文件,用于访问androidResource文件的整数标识符。这是R.java类的c# /托管版本,允许使用这些标识符作为普通的c#字段(有时是const),而无需与Java进行任何互操作。

在一个android Studio“库”项目中,当你包含一个像res/drawable/foo.png这样的文件时,你会得到一个像这样的字段:

package com.yourlibrary;
public class R{    public class drawable{        // The actual integer here maps to a table inside the final .apk file        public final int foo = 1234;    }}

你可以使用这个值,例如,在ImageView中显示这个图像:

ImageView imageView = new ImageView(this);imageView.setImageResource(R.drawable.foo);

当你构建com.yourlibrary.aar时, android的gradle插件实际上并没有把这个类放在包中。相反,android应用程序实际上知道整数的值是多少。因此,R类是在android应用程序构建时生成的,为每个android库生成一个R类。

Xamarin.Android采取了不同的方法,在运行时进行整数修复。用c#和MSBuild做这样的事情真的没有一个很好的先例吗?例如,一个c# android库可能有:

public class Resource{    public class Drawable    {        // The actual integer here is *not* final        public int foo = -1;    }}

然后主应用程序就会有如下代码:

public class Resource{    public class Drawable    {        public Drawable(){            // Copy the value at runtime            global::MyLibrary.Resource.Drawable.foo = foo;        }
        // The actual integer here *is* final        public const int foo = 1234;    }}

这种情况已经很好地运行了一段时间,但不幸的是,像androidX、Material、谷歌Play Services等谷歌的库中的资源数量已经开始复合。例如,在dotnet/maui#2606中,启动时设置了21497个字段!我们创建了一种方法来解决这个问题,但我们也有一个新的自定义修剪步骤来执行修复在构建时(在修剪期间)而不是在运行时。

<AndroidLinkResources>true</ AndroidLinkResources>

这将使你的版本版本替换案例如下:

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

相反,直接内联整数:

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

这个特性的一个已知问题是:

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

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

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

查看xamarin-android#5317, xamarin-android#6696,和dotnet/maui#4912了解该功能的详细信息。

  • 修剪Resource.designer.cs:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#trimming-resourcedesignercs?ocid=AID3045631

  • dotnet/maui#2606:

    https://github.com/dotnet/maui/pull/2606

  • xamarin-android#5317:

    https://github.com/xamarin/xamarin-android/pull/5317

  • xamarin-android#6696:

    https://github.com/xamarin/xamarin-android/pull/6696

  • 和dotnet/maui#4912:

    https://github.com/dotnet/maui/pull/4912

▌R8 Java代码收缩器

R8是全程序优化、收缩和缩小工具,将java字节代码转换为优化的dex代码。R8使用Proguard keep规则格式为应用程序指定入口点。如您所料,许多应用程序需要额外的Proguard规则来保持工作。R8可能过于激进,并且删除了Java反射所调用的一些东西,等等。我们还没有一个很好的方法让它成为所有.NET android应用程序的默认设置。

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

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

如果启动你的应用程序的Release构建在启用后崩溃,检查adb logcat输出,看看哪里出了问题。

如果你看到java.lang. classnotfoundexception或java.lang。你可能需要添加一个ProguardConfiguration文件到你的项目中,比如:

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

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

详情请参阅我们的D8/R8文档。

  • R8 Java代码收缩器:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#r8-java-code-shrinker?ocid=AID3045631

  • 我们的D8/R8文档:
    https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/D8andR8.md
▌AOT

Profiled AOT是默认的,因为它在应用程序大小和启动性能之间给出了最好的权衡。如果应用程序的大小与你的应用程序无关,你可以考虑对所有.NET程序集使用AOT。

要选择加入,在你的.csproj中添加以下Release配置:

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

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

  • AOT:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-everything?ocid=AID3045631
▌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的文档。

  • AOT和LLVM:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#aot-and-llvm?ocid=AID3045631

  • LLVM:
    https://llvm.org/
  • EnableLLVM的文档:

    https://docs.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#enablellvm?ocid=AID3045631

▌记录自定义AOT配置文件

概要AOT默认使用我们在.NET MAUI和android工作负载中提供的“内置”概要文件,对大多数应用程序都很有用。为了获得最佳的启动性能,理想情况下应该记录应用程序特定的配置文件。针对这种情况,我们有一个实验性的Mono.Profiler.Android包。

记录配置文件:

dotnet add package Mono.AotProfiler.androiddotnet build -t:BuildAndStartAotProfiling# Wait until app launches, or you navigate to a screendotnet build -t:FinishAotProfiling

这将在你的项目目录下产生一个custom.aprof。要在未来的构建中使用它:

<ItemGroup>  <androidAotProfile Include="custom.aprof" /></ItemGroup>

我们正在努力在未来的.NET版本中完全支持记录自定义概要文件。

  • 记录自定义AOT配置文件:

    https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#record-a-custom-aot-profile?ocid=AID3045631

  • Mono.Profiler.Android:
    https://github.com/jonathanpeppers/Mono.Profiler.Android

希望您喜欢我们的.NET MAUI性能论述。请尝试.NET MAUI并且可以在http://dot.net/maui了解更多!