性能分析

性能分析是衡量应用程序性能以识别改进领域的过程。 通常,.NET MAUI 和客户端应用程序都对以下方面感兴趣:

  • 启动时间:应用程序启动和显示第一个屏幕所需的时间。
  • CPU 使用率:如果特定方法通过许多调用或长时间运行的操作消耗过多的 CPU 时间。
  • 内存使用率:如果有很多不合理的分配或内存泄漏。

用于改进这些指标的技术和工具是不同的,我们计划在本指南中揭秘这些指标。 用于分析 .NET MAUI 应用程序的工具也可能因平台而异。 本指南介绍 Android、iOS、Mac Catalyst 和 Windows 分析方法。

重要

始终对 Release 进行性能分析,以便准确测量性能。 Debug 生成使用解释器(UseInterpreter=true)用于支持 C# 热重载,这极大地影响性能并导致不切实际的结果。

先决条件

安装诊断工具

若要在 iOS 和 Android 上分析 .NET MAUI 应用程序,需要安装以下 .NET 全局工具:

可以使用以下命令安装这些工具:

$ 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-tracedotnet-dsrouterdotnet-gcdump 以获取最新版本。

从版本 9.0.652701 开始,dotnet-tracedotnet-gcdump 都包括一个选项,可以自动启动和管理 dotnet-dsrouter 作为子进程。 这样就无需单独运行 dotnet-dsrouter ,从而大大简化了分析工作流。

有关使用这些工具的实时演示,请参阅 .NET Conf 会话( 使用 AI 的 .NET 诊断工具)。

工具如何协同工作

若要在 iOS 和 Android 上使用这些诊断工具,多个组件协同工作:

  • .NET 全局工具 (dotnet-tracedotnet-gcdumpdotnet-dsrouter) 在开发计算机上运行
  • Mono 诊断组件 (libmono-component-diagnostics_tracing.so) 包含在应用程序包中
  • dotnet-dsrouter 将诊断连接从远程设备或模拟器转发到计算机上的本地端口
  • 诊断工具连接到此本地端口以收集分析数据

--dsrouter选项在dotnet-tracedotnet-gcdump中自动处理启动dotnet-dsrouter以及协调连接的复杂性。

构建用于性能分析的应用程序

若要启用分析,必须使用特殊的 MSBuild 属性生成应用程序,这些属性包括诊断组件并配置与分析工具的连接。

了解诊断属性

以下 MSBuild 属性控制应用程序与诊断工具的通信方式:

  • DiagnosticAddress:正在侦听的 dotnet-dsrouter IP 地址。 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 属性。

分析启动时间

若要捕获准确的启动时间度量,请暂停应用程序启动,直到探查器准备就绪。 这可确保从最开始就捕获整个启动序列。

  1. 在一个终端中,使用--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

  2. 在另一个终端中,构建并部署您的应用程序,在启动时暂停:-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
    
  3. 应用程序将在初始屏幕上暂停,等待 dotnet-trace 连接。 连接后,应用程序将启动并开始 dotnet-trace 录制。

  4. 允许应用程序完全启动并到达初始屏幕。

  5. 请在dotnet-trace终端按<Enter>键以停止录制。

跟踪文件将保存到当前目录。 使用 -o 此选项可以指定不同的输出目录。

运行时操作的性能分析

若要在运行时分析特定操作(例如按钮轻触、导航或滚动),在应用程序启动后使用 -p:DiagnosticSuspend=false 并连接探查器。

  1. 使用-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
    
  2. 导航到要分析的应用程序区域。

  3. dotnet-trace选项--dsrouter开始:

    dotnet-trace collect --dsrouter android --format speedscope
    
  4. 执行您想要分析的操作。

  5. <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 进行分析:

  1. 构建面向Release并已启用ReadyToRun的应用程序:

    dotnet publish -f net10.0-windows10.0.19041.0 -c Release -p:PublishReadyToRun=true
    
  2. 启动 PerfView 并选择 Collect>Collect

  3. “命令 ”字段中,筛选应用的可执行文件(例如, hellomaui.exe)。

  4. 单击“ 开始集合”,然后手动启动应用。

  5. 在您的应用程序完成要分析的操作后,单击 停止收集

  6. 打开 CPU 堆栈 以查看计时信息,或使用 “火焰图 ”选项卡进行图形视图。

还可以以 SpeedScope 格式(File>Save View As)保存 PerfView 数据,以便在 https://speedscope.app/ 查看它,进行跨平台分析。

使用 PerfView 测量 Windows 启动时间

若要测量 Windows 上的精确启动时间,可以使用 PerfView 捕获 Windows 事件跟踪(ETW) 事件:

  1. 在 PerfView 中,打开 Collect>Collect 并展开 “高级选项”。

  2. 配置以下项目:

    • 启用 Kernel Base
    • Microsoft-Windows-XAML:0x44:Informational添加到其他提供程序
  3. 单击“ 开始集合”,然后启动并关闭应用 3-5 次。

  4. 单击“ 停止收集”。

  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 进行应用启动分析

  1. Release 构建应用,保留符号。

    dotnet build -c Release -f net10.0-ios -p:NoSymbolStrip=true
    

    NoSymbolStrip=true 属性在可执行文件中保留本机符号,使 Instruments 中的堆栈跟踪更加有用。

  2. 在设备上安装应用:

    dotnet build -t:Run -c Release -f net10.0-ios -p:NoSymbolStrip=true
    
  3. 启动工具(从 Xcode 或通过在终端中运行 open -a Instruments )。

  4. 在顶部选择你的 iOS 设备。

  5. 从已安装的应用程序列表中选择你的应用。

  6. 选择 应用启动 工具模板。

  7. 单击“ 选择”,然后单击“ 记录 ”按钮开始分析。

  8. 应用将自动启动。 应用完全启动后停止录制。

  9. 在结果中,选择 “应用生命周期 ”行以查看生命周期时间线。 底部表中的最后一行显示应用完成启动的时间(例如, Currently running in the foreground...) 。

有关使用 Instruments 的详细信息,请参阅 Apple 有关 减少应用的启动时间的文档。

分析内存使用情况

内存分析可帮助你识别内存泄漏并了解应用程序中的内存分配模式。 使用 dotnet-gcdump 创建托管内存快照。

收集内存转储

若要收集内存转储,请使用如同--dsrouterdotnet-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 由于内存消耗过多而终止应用程序。

内存泄漏的症状

内存泄漏的典型症状可能是:

  1. 从主页导航到详细信息页
  2. 返回
  3. 再次导航到详细信息页
  4. 内存随每个周期一致增长

确定是否存在泄漏

若要确定页面是否实际泄漏,可在调试期间通过使用终结器和日志记录,并强制进行垃圾回收。

  1. 将具有日志记录的终结器添加到页面类:

    ~MyDetailsPage() => System.Diagnostics.Debug.WriteLine("~MyDetailsPage() finalized");
    
  2. 在战略位置强制垃圾回收(仅用于调试):

    public MyDetailsPage()
    {
        GC.Collect(); // For debugging purposes only
        GC.WaitForPendingFinalizers();
        InitializeComponent();
    }
    
  3. 测试Release构建并使用adb logcat(Android)或设备日志(iOS)查看控制台输出。

如果在离开页面时运行终结器,则页面被正确收集。 如果终结器从不运行,则说明页面出现了泄露——因为有某个元素无限期地保留对它的引用。

警告

在调试后删除 GC.Collect() 调用。 它们仅用于诊断问题,不应位于生产代码中。

缩小原因范围

确定泄漏后,请缩小原因范围:

  1. 注释掉所有 XAML 内容。 泄漏是否仍然存在?
  2. 注释掉 code-behind 中的所有 C# 代码。 泄漏是否仍然存在?
  3. 在多个平台上进行测试。 它是否仅在一个平台上发生?

通常,空 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) { }
}

解决方法

  1. 定义事件处理程序 static

    static void OnValueChanged(object? sender, EventArgs e) { }
    
  2. 使用未从 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 文档

基于日志的启动度量

对于测量所有平台的启动时间的轻型方法,可以在应用程序中的特定点记录消息并测量它们之间的时间:

  1. 在主页加载时添加日志消息:

    Loaded += (sender, e) => Dispatcher.Dispatch(() => 
        Console.WriteLine("loaded"));
    
  2. 使用示例工具,如measure-startup,来启动您的应用并测量日志消息显示之前的时间。

  3. 在 Android 上,可以筛选 adb logcat 输出来监视特定消息:

    adb logcat | grep "loaded"
    

此方法适用于所有平台,适用于持续集成方案或快速检查。

其他资源