性能分析是衡量应用程序性能以识别改进领域的过程。 通常,.NET MAUI 和客户端应用程序都对以下方面感兴趣:
- 启动时间:应用程序启动和显示第一个屏幕所需的时间。
- CPU 使用率:如果特定方法通过许多调用或长时间运行的操作消耗过多的 CPU 时间。
- 内存使用率:如果有很多不合理的分配或内存泄漏。
用于改进这些指标的技术和工具是不同的,我们计划在本指南中揭秘这些指标。 用于分析 .NET MAUI 应用程序的工具也可能因平台而异。 本指南介绍 Android、iOS、Mac Catalyst 和 Windows 分析方法。
重要
始终对 Release 进行性能分析,以便准确测量性能。
Debug 生成使用解释器(UseInterpreter=true)用于支持 C# 热重载,这极大地影响性能并导致不切实际的结果。
先决条件
安装诊断工具
若要在 iOS 和 Android 上分析 .NET MAUI 应用程序,需要安装以下 .NET 全局工具:
-
dotnet-trace- 收集 CPU 跟踪和性能数据 -
dotnet-dsrouter- 将诊断连接从远程设备转发到本地计算机 -
dotnet-gcdump- 收集内存转储以分析托管内存使用情况
可以使用以下命令安装这些工具:
$ dotnet tool install -g dotnet-trace
You can invoke the tool using the following command: dotnet-trace
Tool 'dotnet-trace' was successfully installed.
$ dotnet tool install -g dotnet-dsrouter
You can invoke the tool using the following command: dotnet-dsrouter
Tool 'dotnet-dsrouter' was successfully installed.
$ dotnet tool install -g dotnet-gcdump
You can invoke the tool using the following command: dotnet-gcdump
Tool 'dotnet-gcdump' was successfully installed.
注释
至少需要所有诊断工具版本 9.0.652701 才能使用本指南中所述的功能。 检查 NuGet 上的 dotnet-trace、 dotnet-dsrouter 和 dotnet-gcdump 以获取最新版本。
从版本 9.0.652701 开始,dotnet-trace 和 dotnet-gcdump 都包括一个选项,可以自动启动和管理 dotnet-dsrouter 作为子进程。 这样就无需单独运行 dotnet-dsrouter ,从而大大简化了分析工作流。
有关使用这些工具的实时演示,请参阅 .NET Conf 会话( 使用 AI 的 .NET 诊断工具)。
工具如何协同工作
若要在 iOS 和 Android 上使用这些诊断工具,多个组件协同工作:
- .NET 全局工具 (
dotnet-trace,dotnet-gcdump,dotnet-dsrouter) 在开发计算机上运行 - Mono 诊断组件 (
libmono-component-diagnostics_tracing.so) 包含在应用程序包中 -
dotnet-dsrouter将诊断连接从远程设备或模拟器转发到计算机上的本地端口 - 诊断工具连接到此本地端口以收集分析数据
--dsrouter选项在dotnet-trace和dotnet-gcdump中自动处理启动dotnet-dsrouter以及协调连接的复杂性。
构建用于性能分析的应用程序
若要启用分析,必须使用特殊的 MSBuild 属性生成应用程序,这些属性包括诊断组件并配置与分析工具的连接。
了解诊断属性
以下 MSBuild 属性控制应用程序与诊断工具的通信方式:
DiagnosticAddress:正在侦听的dotnet-dsrouterIP 地址。10.0.2.2用于 Android 仿真器(从仿真器的角度来看,这是主机的回环地址),127.0.0.1用于物理设备和 iOS。DiagnosticPort:诊断连接的端口号(默认值为9000)。DiagnosticSuspend:当true时,应用程序在启动前会等待探查器连接。 当false应用程序立即启动,探查器稍后可以连接。 用于true启动分析,false用于运行时分析和内存转储。DiagnosticListenMode:将connect设置为 Android(应用程序连接到dotnet-dsrouter),或者将listen设置为 iOS(应用程序侦听dotnet-dsrouter以进行连接)。EnableDiagnostics:当true时,在应用程序包中包含 Mono 诊断组件。 设置任何Diagnostic*MSBuild 属性时,会隐式设置此设置。 此属性适用于 Android、iOS 和 Mac Catalyst。
注释
使用 CoreCLR(目前在 Android 上为实验性,并计划支持 iOS),诊断组件内置在运行时中,不需要EnableDiagnostics。
生成命令示例
运行dotnet-trace或使用dotnet-gcdump--dsrouter选项时,该工具会显示生成应用程序的说明。
例如:
对于 Android 模拟器:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=10.0.2.2 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
对于 Android 设备:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
对于 iOS 设备和模拟器:
dotnet build -t:Run -c Release -f net10.0-ios -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=listen
注释
在$(TargetFrameworks)中,对于具有多个目标框架的项目,使用-f net10.0-android或-f net10.0-ios。
重要
使用这些诊断属性生成的应用程序只能用于开发和测试。 绝对不要发布启用了诊断组件的版本到生产环境,因为它们可以公开终结点,并提供对应用程序代码的更深入的洞察。
分析 CPU 使用率
该工具 dotnet-trace 以类似 .nettrace 和 .speedscope.json. 格式收集 CPU 采样信息。 这些跟踪显示每个方法花费的时间,帮助你确定应用程序中的性能瓶颈。
CPU 分析的工作流取决于是分析启动时间还是分析运行时操作。 主要区别是 -p:DiagnosticSuspend MSBuild 属性。
分析启动时间
若要捕获准确的启动时间度量,请暂停应用程序启动,直到探查器准备就绪。 这可确保从最开始就捕获整个启动序列。
在一个终端中,使用
--dsrouter选项启动dotnet-trace:dotnet-trace collect --dsrouter android-emu --format speedscope或者对于物理 Android 设备:
dotnet-trace collect --dsrouter android --format speedscope对于 iOS 设备和模拟器,请分别使用
--dsrouter ios或--dsrouter ios-sim。在另一个终端中,构建并部署您的应用程序,在启动时暂停:
-p:DiagnosticSuspend=true对于 Android 模拟器:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=10.0.2.2 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=connect对于 Android 设备:
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=connect对于 iOS(设备和模拟器):
dotnet build -t:Run -c Release -f net10.0-ios -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=true -p:DiagnosticListenMode=listen应用程序将在初始屏幕上暂停,等待
dotnet-trace连接。 连接后,应用程序将启动并开始dotnet-trace录制。允许应用程序完全启动并到达初始屏幕。
请在
dotnet-trace终端按<Enter>键以停止录制。
跟踪文件将保存到当前目录。 使用 -o 此选项可以指定不同的输出目录。
运行时操作的性能分析
若要在运行时分析特定操作(例如按钮轻触、导航或滚动),在应用程序启动后使用 -p:DiagnosticSuspend=false 并连接探查器。
使用
-p:DiagnosticSuspend=false构建和部署您的应用程序:dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect导航到要分析的应用程序区域。
从
dotnet-trace选项--dsrouter开始:dotnet-trace collect --dsrouter android --format speedscope执行您想要分析的操作。
按
<Enter>以停止跟踪。
此方法创建一个更集中的跟踪文件,该文件仅包含您正在调查的特定操作。
了解追踪结果
收集跟踪时 dotnet-trace ,你将看到类似于以下内容的输出:
Process : $HOME/.dotnet/tools/dotnet-dsrouter
Output File : /tmp/hellomaui-app-trace
[00:00:00:35] Recording trace 1.7997 (MB)
Press <Enter> or <Ctrl+C> to exit...
按 <Enter>后,跟踪将完成。
Stopping the trace. This may take up to minutes depending on the application being traced.
Trace completed.
Writing: hellomaui-app-trace.speedscope.json
查看跟踪文件
参数 --format 控制输出格式:
-
nettrace(默认值):可以在 PerfView 或 Windows 上的 Visual Studio 中查看 -
speedscope:可在任何平台上查看的 JSON 格式 https://speedscope.app/
对于跨平台分析,请使用 --format speedscope:
dotnet-trace collect --dsrouter android --format speedscope
Windows 上的分析
虽然跨平台 dotnet-trace 工具适用于 Windows,但该平台提供了可能更方便的本地性能分析选项。
使用 Visual Studio 性能探查器
Visual Studio 性能探查器为 .NET 应用程序提供集成分析。 如需全面指南,请参阅 Visual Studio 分析功能教程。
使用 PerfView
PerfView 是一种功能强大的免费性能分析工具,适用于 Windows,可分析设置最少的 .NET MAUI 应用程序。
使用 PerfView 进行分析:
构建面向
Release并已启用ReadyToRun的应用程序:dotnet publish -f net10.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=true启动 PerfView 并选择
Collect>Collect。在 “命令 ”字段中,筛选应用的可执行文件(例如,
hellomaui.exe)。单击“ 开始集合”,然后手动启动应用。
在您的应用程序完成要分析的操作后,单击 停止收集。
打开 CPU 堆栈 以查看计时信息,或使用 “火焰图 ”选项卡进行图形视图。
还可以以 SpeedScope 格式(File>Save View As)保存 PerfView 数据,以便在 https://speedscope.app/ 查看它,进行跨平台分析。
使用 PerfView 测量 Windows 启动时间
若要测量 Windows 上的精确启动时间,可以使用 PerfView 捕获 Windows 事件跟踪(ETW) 事件:
在 PerfView 中,打开
Collect>Collect并展开 “高级选项”。配置以下项目:
- 启用 Kernel Base
- 将
Microsoft-Windows-XAML:0x44:Informational添加到其他提供程序
单击“ 开始集合”,然后启动并关闭应用 3-5 次。
单击“ 停止收集”。
打开 “事件” 报告,并通过查找计算启动时间:
-
Windows Kernel/Process/Start事件(注意Time MSec值)适用于您的应用 - 同一进程 ID 的第一个
Microsoft-Windows-XAML/Frame/Stop事件 - 从停止时间减去开始时间以获取启动持续时间
-
多次运行应用,并平均结果以获取更准确的度量值。
在 Windows 上使用 dotnet-trace
对于未打包的 Windows 应用程序,可以直接使用 dotnet-trace :
dotnet publish -f net10.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=true -p:WindowsPackageType=None
dotnet trace collect --format speedscope -- bin\Release\net10.0-windows10.0.19041.0\win10-x64\publish\YourApp.exe
在 iOS 和 Mac Catalyst 上使用 Instruments 进行分析
对于 iOS 和 Mac Catalyst 应用程序,Apple 的 Instruments 工具提供本机分析,并详细介绍了应用启动时间和性能。
使用 Instruments 进行应用启动分析
为
Release构建应用,保留符号。dotnet build -c Release -f net10.0-ios -p:NoSymbolStrip=true该
NoSymbolStrip=true属性在可执行文件中保留本机符号,使 Instruments 中的堆栈跟踪更加有用。在设备上安装应用:
dotnet build -t:Run -c Release -f net10.0-ios -p:NoSymbolStrip=true启动工具(从 Xcode 或通过在终端中运行
open -a Instruments)。在顶部选择你的 iOS 设备。
从已安装的应用程序列表中选择你的应用。
选择 应用启动 工具模板。
单击“ 选择”,然后单击“ 记录 ”按钮开始分析。
应用将自动启动。 应用完全启动后停止录制。
在结果中,选择 “应用生命周期 ”行以查看生命周期时间线。 底部表中的最后一行显示应用完成启动的时间(例如,
Currently running in the foreground...) 。
有关使用 Instruments 的详细信息,请参阅 Apple 有关 减少应用的启动时间的文档。
分析内存使用情况
内存分析可帮助你识别内存泄漏并了解应用程序中的内存分配模式。 使用 dotnet-gcdump 创建托管内存快照。
收集内存转储
若要收集内存转储,请使用如同--dsrouter的dotnet-trace工作流。
dotnet-gcdump collect --dsrouter android
使用--dsrouter android-emu或--dsrouter ios--dsrouter ios-sim用于其他目标。
与 CPU 跟踪不同,内存转储不需要暂停应用程序启动。 使用 -p:DiagnosticSuspend=false 构建应用程序。
dotnet build -t:Run -c Release -f net10.0-android -p:DiagnosticAddress=127.0.0.1 -p:DiagnosticPort=9000 -p:DiagnosticSuspend=false -p:DiagnosticListenMode=connect
连接后 dotnet-gcdump ,它会在当前目录中创建一个 *.gcdump 文件。 可以在 Windows 上的 Visual Studio 或 PerfView 中打开此文件。
分析内存转储
在 Visual Studio 中打开 *.gcdump 文件时,可以:
- 查看内存中的每个托管对象
- 查看每种类型的总数和大小
- 检查引用树以了解使对象保持活动状态的内容
- 比较多个快照以确定不断增长的分配
Visual Studio 的内存使用情况诊断工具(Debug>Windows>Diagnostic Tools)还允许你在调试时拍摄快照,不过应禁用 XAML 热重载以获取准确的结果。
小窍门
请考虑为 Release 构建创建内存快照,因为启用 XAML 编译、AOT 编译和代码修剪时,代码路径可能会大不相同。
诊断内存泄漏
.NET MAUI 应用程序中的内存泄漏表现为内存使用量稳步增加,尤其是在重复导航或交互期间。 在移动平台上,这最终可能会导致 OS 由于内存消耗过多而终止应用程序。
内存泄漏的症状
内存泄漏的典型症状可能是:
- 从主页导航到详细信息页
- 返回
- 再次导航到详细信息页
- 内存随每个周期一致增长
确定是否存在泄漏
若要确定页面是否实际泄漏,可在调试期间通过使用终结器和日志记录,并强制进行垃圾回收。
将具有日志记录的终结器添加到页面类:
~MyDetailsPage() => System.Diagnostics.Debug.WriteLine("~MyDetailsPage() finalized");在战略位置强制垃圾回收(仅用于调试):
public MyDetailsPage() { GC.Collect(); // For debugging purposes only GC.WaitForPendingFinalizers(); InitializeComponent(); }测试
Release构建并使用adb logcat(Android)或设备日志(iOS)查看控制台输出。
如果在离开页面时运行终结器,则页面被正确收集。 如果终结器从不运行,则说明页面出现了泄露——因为有某个元素无限期地保留对它的引用。
警告
在调试后删除 GC.Collect() 调用。 它们仅用于诊断问题,不应位于生产代码中。
缩小原因范围
确定泄漏后,请缩小原因范围:
- 注释掉所有 XAML 内容。 泄漏是否仍然存在?
- 注释掉 code-behind 中的所有 C# 代码。 泄漏是否仍然存在?
- 在多个平台上进行测试。 它是否仅在一个平台上发生?
通常,空 ContentPage 不应泄漏。 通过系统地删除代码,可以识别导致问题的控件或代码模式。
常见泄漏模式
C# 事件
C# 事件可以创建循环引用来防止垃圾回收。 假设子对象订阅父对象的事件,但父对象还持有对子对象的引用。 这两个对象最终会永远存在。
如果事件源的生命周期比订阅者长(例如在Application.Resources中的Style),这可能会导致整个页面泄漏。
解决方案:在 .NET MAUI 控件中使用 WeakEventManager 处理事件,或在对象不再需要时取消订阅事件。
iOS 和 Mac Catalyst 循环引用
在 iOS 和 Mac Catalyst 上,C# 对象和本机对象之间的循环引用可能会导致内存泄漏,因为子类 NSObject 的 C# 对象同时存在于垃圾回收的 .NET 世界和引用计数的 Objective-C 世界中。
有问题的模式的示例:
class MyView : UIView
{
public MyView()
{
var picker = new UIDatePicker();
AddSubview(picker); // MyView -> UIDatePicker
picker.ValueChanged += OnValueChanged; // UIDatePicker -> MyView via event handler
}
void OnValueChanged(object? sender, EventArgs e) { }
}
解决方法:
定义事件处理程序
static:static void OnValueChanged(object? sender, EventArgs e) { }使用未从
NSObject继承的代理对象:class MyView : UIView { readonly Proxy _proxy = new(); public MyView() { var picker = new UIDatePicker(); AddSubview(picker); picker.ValueChanged += _proxy.OnValueChanged; } class Proxy { public void OnValueChanged(object? sender, EventArgs e) { } } }
注释
这些循环参考问题特定于 iOS 和 Mac Catalyst。 它们通常不会出现在 Android 或 Windows 上。
避免泄漏的最佳做法
测试
Release构建:由于优化、修整和 AOT 编译,内存行为可能与Debug构建有很大差异。调查时使用终结器:将具有日志记录的终结器添加到关键对象,以快速识别它们是否正在收集。
取消订阅事件:在对象被释放或不再需要时始终取消订阅事件。
对长生存期对象的事件保持谨慎:避免使用长生存期对象(如中
Application.Resources对象)保留对短期对象的引用(如页面或视图)。定期分析:使内存分析成为常规测试过程的一部分,尤其是在添加新功能或进行重大更改之后。
有关内存泄漏模式和技术的更多详细信息,请参阅 .NET MAUI 内存泄漏 wiki。
替代分析方法
Android ActivityManager 启动日志
Android 通过 ActivityManager 自动记录启动时间信息。 可以使用以下方法 adb logcat查看这些日志:
adb logcat | grep "ActivityManager"
应用启动时,你将看到如下消息:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
这显示了显示活动所花费的时间。 这是一种快速测量启动时间的方法,无需进行任何额外的工具或代码更改。
有关 Android 应用启动时间和优化技术的详细信息,请参阅 有关应用启动时间的 Android 文档。
基于日志的启动度量
对于测量所有平台的启动时间的轻型方法,可以在应用程序中的特定点记录消息并测量它们之间的时间:
在主页加载时添加日志消息:
Loaded += (sender, e) => Dispatcher.Dispatch(() => Console.WriteLine("loaded"));使用示例工具,如measure-startup,来启动您的应用并测量日志消息显示之前的时间。
在 Android 上,可以筛选
adb logcat输出来监视特定消息:adb logcat | grep "loaded"
此方法适用于所有平台,适用于持续集成方案或快速检查。
其他资源
- .NET MAUI 性能分析 Wiki - 包含高级方案和疑难解答的综合 Wiki
- Android 跟踪指南 - 特定于 Android 的详细分析说明
- iOS/macOS 性能分析 Wiki - Apple 平台的专用指南
-
.NET 诊断工具文档 -
dotnet-trace、dotnet-dsrouter和dotnet-gcdump的官方文档 - PerfView 用户指南 - 有关将 PerfView 用于 Windows 分析的深入指南