(本文阅读时间:9 分钟)
继上一篇文章🔗为大家介绍了启动性能的优化,今天我们来看一看其他令人欣喜的性能提升。
主要内容
❖ 应用程序大小的改进
❖ .NET Podcast示例中的改进
❖ 实验性或高级选项
应用程序大小的改进
▌修复默认的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应用程序能够完全删除几个程序集:
查看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的文档。
▌记录自定义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了解更多!