应用的启动性能最佳做法

通过改进处理启动和激活的方式,创建具有最佳启动时间的通用 Windows 平台(UWP)应用。

应用的启动性能最佳做法

部分用户根据启动需要多长时间来感知你的应用是快速还是慢速。 出于本主题的目的,应用的启动时间从用户启动应用时开始,当用户可以通过某种有意义的方式与应用交互时结束。 本部分提供有关如何在应用启动时获得更好的性能的建议。

测量应用的启动时间

在实际测量应用的启动时间之前,请务必多次启动应用。 这为你提供了测量的基准,并确保你测得的启动时间尽可能短。

在 UWP 应用到达客户的计算机上时,你的应用已使用 .NET Native 工具链进行编译。 .NET Native 是一种将 MSIL 转换为本机可运行的计算机代码的预先编译技术。 .NET Native 应用启动速度更快,使用更少的内存,使用比 MSIL 对应应用更少的电池。 构建于 .NET Native 的应用程序会静态链接到自定义运行时和新的统一 .NET Core 系统,这些系统可以在所有设备上运行,因此它们不依赖于内置的 .NET 实现。 在开发计算机上,如果你在“发布”模式下生成该应用,则应用默认使用 .NET Native,如果在“调试”模式下生成它,则使用 CoreCLR。 可以在 Visual Studio 中,从“属性”(C#)的“生成”页或在“我的项目”(VB)中的“编译-高级”选项进行配置。 查找显示“使用 .NET 本机工具链进行编译”的复选框。

当然,你应该进行能够反映最终用户体验的测量。 因此,如果你不确定是否在开发计算机上将应用编译为原生代码,则可以运行本机映像生成器(Ngen.exe)工具,在测量应用启动时间之前预先编译应用。

以下过程介绍如何运行 Ngen.exe 来预编译应用。

运行 Ngen.exe

  1. 至少运行应用一次,以确保 Ngen.exe 检测到它。

  2. 通过以下操作之一打开 任务计划程序

    • 从开始屏幕中搜索“任务计划程序”。
    • 运行“taskschd.msc”。
  3. 任务计划程序的左侧窗格中,展开 任务计划程序库

  4. 展开 Microsoft。

  5. 展开 Windows。

  6. 选择 .NET Framework

  7. 从任务列表中选择 .NET Framework NGEN 4.x

    如果使用 64 位计算机,则还有 .NET Framework NGEN v4.x 64。 如果要生成 64 位应用,请选择。NET Framework NGEN v4.x 64

  8. “操作”菜单中,单击“运行”

Ngen.exe 预编译计算机上已使用且没有本机映像的所有应用。 如果有很多应用需要预编译,这可能需要很长时间,但后续运行要快得多。

重新编译你的应用程序时,本机映像就不再被使用。 相反,应用是实时编译的,这意味着它是在应用运行时编译的。 必须再次运行 Ngen.exe 才能生成新的本地图像。

尽可能延迟工作

若要改进应用的启动时间,请仅执行绝对需要完成的工作,让用户开始与应用交互。 如果你能延迟加载其他程序集,这可能特别有用。 公共语言运行时在首次使用程序集时加载程序集。 如果可以最大程度地减少加载的程序集数,则也许能够改善应用的启动时间和内存消耗。

独立执行长时间运行的工作

即使应用的某些部分功能不完全正常运行,应用也可以是交互式应用。 例如,如果应用显示检索需要一段时间的数据,则可以通过异步检索数据使该代码独立于应用的启动代码执行。 当数据可用时,使用数据填充应用的用户界面。

检索数据的许多通用 Windows 平台 (UWP) API 都是异步的,因此你仍可能异步检索数据。 有关异步 API 的详细信息,请参阅 在 C# 或 Visual Basic 中调用异步 API。 如果执行的工作不使用异步 API,则可以使用 Task 类执行长时间运行的工作,以免阻止用户与应用交互。 这会让应用在数据加载时对用户做出响应。

如果你的应用加载部分 UI 需要很长时间,请考虑在该区域中添加一个字符串,其中显示类似“获取最新数据”的内容,以便你的用户知道该应用仍在处理。

最小化启动时间

除了最简单的应用,还需要一定的时间来加载资源、分析 XAML、设置数据结构并在激活时运行逻辑。 在这里,我们通过将其分为三个阶段来分析激活过程。 我们还提供有关减少每个阶段花费的时间的提示,以及使应用启动的每个阶段对用户更具可口性的技术。

激活期是用户启动应用和应用正常运行的时刻之间的时间。 这是一个关键时刻,因为它是用户对应用的第一印象。 他们期望系统和应用提供即时和持续反馈。 当应用无法快速启动时,系统和应用被视为损坏或设计不佳。 更糟的是,如果应用激活时间过长,进程生存期管理器(PLM)可能会终止它,或者用户可能会卸载它。

启动阶段简介

启动涉及许多移动部分,所有这些作都需要正确协调,以获得最佳用户体验。 在用户单击应用磁贴和显示的应用程序内容之间,将执行以下步骤。

  • Windows shell 启动进程,并调用 Main。
  • 创建 Application 对象。
    • (项目模板)构造函数调用 InitializeComponent,这会导致 App.xaml 被解析,并创建对象。
  • 触发应用程序的 OnLaunched 事件。
    • (ProjectTemplate)应用代码创建 Frame 并导航到 MainPage。
    • (ProjectTemplate)Mainpage 构造函数调用 InitializeComponent,这会导致 MainPage.xaml 进行分析并创建对象。
    • ProjectTemplate) Window.Current.Activate() 被调用。
  • XAML 平台运行布局过程,包括测量和排列。
    • ApplyTemplate 将为每个控件创建控件模板内容,这通常是在启动时布局耗时的大部分。
  • 调用渲染来为所有窗口内容创建视觉效果。
  • 框架呈现给桌面 Windows 管理器(DWM)。

在启动路径中少执行操作

确保启动代码路径中不包含启动第一帧不需要的任何内容。

  • 如果用户 dll 包含第一帧期间不需要的控件,请考虑延迟加载它们。
  • 如果 UI 中有一部分依赖于云中的数据,则拆分该 UI。 首先,打开不依赖于云数据的 UI,并异步启动依赖于云的 UI。 还应考虑在本地缓存数据,以便应用程序脱机工作,或者不受网络连接速度缓慢的影响。
  • 如果 UI 正在等待数据,则显示进度指示器。
  • 请谨慎处理涉及大量解析配置文件的应用设计,或者由代码动态生成的用户界面(UI)。

减少元素计数

XAML 应用中的启动性能与启动期间创建的元素数直接相关。 创建的元素越少,启动应用所需的时间就越少。 作为粗略的基准,请将每个元素的创建时间视为1毫秒。

  • 项目控件中使用的模板可能会产生最大的影响,因为它们被多次使用。 请参阅 ListView 和 GridView UI 优化
  • 将扩展 UserControls 和控件模板,因此还应将这些纳入考虑。
  • 如果创建任何未在屏幕上显示的 XAML,则应证明是否应在启动时创建这些 XAML 片段。

Visual Studio Live Visual Tree 窗口显示树中每个节点的子元素计数。

实时可视化树。

使用延迟。 折叠元素或将其不透明度设置为 0 不会阻止创建元素。 使用 x:Load 或 x:DeferLoadStrategy,可以延迟一段 UI 的加载,并在需要时加载它。 这是一种有效的方法,能够延迟启动屏幕期间不可见的用户界面(UI)的处理,以便在需要时加载,或者作为延迟处理逻辑的一部分。 若要触发加载,只需为元素调用 FindName。 有关示例和详细信息,请参阅 x:Load 属性x:DeferLoadStrategy 属性

虚拟化。 如果你的 UI 中有列表或重复程序内容,则强烈建议你使用 UI 虚拟化。 如果未虚拟化列表 UI,则需要支付前面创建所有元素的费用,这可能会降低启动速度。 请参阅 ListView 和 GridView UI 优化

应用程序性能不仅关系到原始性能,还关系到感知。 改变操作顺序,使视觉效果首先呈现,这通常会让用户觉得应用程序运行更快。 用户会认为当内容显示在屏幕上时,应用程序已经加载完成。 通常,应用程序在启动过程中需要执行多项任务,而不是所有任务都必须用于启动 UI,因此这些任务应延迟处理或优先级低于 UI。

本主题介绍来自动画/电视的“第一帧”,并衡量最终用户看到内容的时间。

改善初创公司形象

让我们使用简单的在线游戏示例来识别启动的每个阶段和不同的技术,以便在整个过程中为用户提供反馈。 对于此示例,激活的第一个阶段是用户点击游戏磁贴和游戏开始运行其代码之间的时间。 在此期间,系统没有任何内容可供显示给用户,甚至无法指示正确的游戏已经启动。 但是,提供启动屏幕可将该内容提供给系统。 然后,游戏通知用户激活的第一个阶段已完成,方法是在开始运行代码时将静态初始屏幕替换为自己的 UI。

激活的第二个阶段包括创建和初始化对游戏至关重要的结构。 如果应用可以使用激活第一阶段后可用的数据快速创建其初始 UI,则第二个阶段是微不足道的,你可以立即显示 UI。 否则,我们建议应用在初始化时显示加载页面。

加载页面的外观由你决定,它可以像显示进度栏或进度环一样简单。 应用的关键点是,它在变得可响应之前就会指示正在执行任务。 对于游戏,它想要显示其初始屏幕,但 UI 要求将某些图像和声音从磁盘加载到内存中。 这些任务需要几秒钟时间,因此应用通过将初始屏幕替换为加载页面来告知用户,该页面显示与游戏主题相关的简单动画。

第三个阶段在游戏具备创建交互式 UI 所需的最小信息集之后开始,该 UI 将替换加载页面。 此时,在线游戏唯一可用的信息是应用从磁盘加载的内容。 游戏可以附带足够的内容来创建交互式 UI;但由于它是一款在线游戏,因此在连接到 Internet 并下载一些附加信息之前,它才会正常运行。 在获得正常运行所需的所有信息之前,用户可以与用户界面进行交互,但需要从网络获取额外数据的功能应显示内容仍在加载的反馈信息。 应用可能需要一些时间才能完全正常运行,因此必须尽快提供该功能。

现在我们已经确定了在线游戏中激活的三个阶段,接下来让我们将它们绑定到实际代码。

阶段 1

在应用启动之前,它需要告知系统要显示为初始屏幕的内容。 它通过向应用清单中的 SplashScreen 元素提供图像和背景色来执行此作,如示例中所示。 应用开始激活后,Windows 会显示此内容。

<Package ...>
  ...
  <Applications>
    <Application ...>
      <VisualElements ...>
        ...
        <SplashScreen Image="Images\splashscreen.png" BackgroundColor="#000000" />
        ...
      </VisualElements>
    </Application>
  </Applications>
</Package>

有关更多信息,请参阅 添加启动画面

仅使用应用的构造函数初始化对应用至关重要的数据结构。 构造函数仅在首次运行应用时调用,不一定每次激活应用。 例如,对于已运行、放置在后台,然后通过搜索合约激活的应用,不会调用构造函数。

阶段 2

激活应用的原因有很多,每个应用可能需要以不同的方式处理。 可以替代 OnActivatedOnCachedFileUpdaterActivatedOnFileActivatedOnFileOpenPickerActivatedOnFileSavePickerActivatedOnLaunchedOnSearchActivatedOnShareTargetActivated 方法来处理激活的每个原因。 应用在这些方法中必须执行的作之一是创建 UI,将其分配给 Window.Content,然后调用 Window.Activate。 此时,启动画面被应用创建的用户界面取代。 这张视觉图可以是加载屏幕,也可以是应用程序的实际用户界面,这取决于激活时是否有足够的信息来创建它。

public partial class App : Application
{
    // A handler for regular activation.
    async protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);

        // Asynchronously restore state based on generic launch.

        // Create the ExtendedSplash screen which serves as a loading page while the
        // reader downloads the section information.
        ExtendedSplash eSplash = new ExtendedSplash();

        // Set the content of the window to the extended splash screen.
        Window.Current.Content = eSplash;

        // Notify the Window that the process of activation is completed
        Window.Current.Activate();
    }

    // a different handler for activation via the search contract
    async protected override void OnSearchActivated(SearchActivatedEventArgs args)
    {
        base.OnSearchActivated(args);

        // Do an asynchronous restore based on Search activation

        // the rest of the code is the same as the OnLaunched method
    }
}

partial class ExtendedSplash : Page
{
    // This is the UIELement that's the game's home page.
    private GameHomePage homePage;

    public ExtendedSplash()
    {
        InitializeComponent();
        homePage = new GameHomePage();
    }

    // Shown for demonstration purposes only.
    // This is typically autogenerated by Visual Studio.
    private void InitializeComponent()
    {
    }
}
    Partial Public Class App
    Inherits Application

    ' A handler for regular activation.
    Protected Overrides Async Sub OnLaunched(ByVal args As LaunchActivatedEventArgs)
        MyBase.OnLaunched(args)

        ' Asynchronously restore state based on generic launch.

        ' Create the ExtendedSplash screen which serves as a loading page while the
        ' reader downloads the section information.
        Dim eSplash As New ExtendedSplash()

        ' Set the content of the window to the extended splash screen.
        Window.Current.Content = eSplash

        ' Notify the Window that the process of activation is completed
        Window.Current.Activate()
    End Sub

    ' a different handler for activation via the search contract
    Protected Overrides Async Sub OnSearchActivated(ByVal args As SearchActivatedEventArgs)
        MyBase.OnSearchActivated(args)

        ' Do an asynchronous restore based on Search activation

        ' the rest of the code is the same as the OnLaunched method
    End Sub
End Class

Partial Friend Class ExtendedSplash
    Inherits Page

    Public Sub New()
        InitializeComponent()

        ' Downloading the data necessary for
        ' initial UI on a background thread.
        Task.Run(Sub() DownloadData())
    End Sub

    Private Sub DownloadData()
        ' Download data to populate the initial UI.

        ' Create the first page.
        Dim firstPage As New MainPage()

        ' Add the data just downloaded to the first page

        ' Replace the loading page, which is currently
        ' set as the window's content, with the initial UI for the app
        Window.Current.Content = firstPage
    End Sub

    ' Shown for demonstration purposes only.
    ' This is typically autogenerated by Visual Studio.
    Private Sub InitializeComponent()
    End Sub
End Class

在激活处理程序显示加载页面的应用程序会开始在后台创建用户界面。 在创建该元素之后,其 FrameworkElement.Loaded 事件就会发生。 在事件处理程序中,将窗口的内容(当前为加载屏幕)替换为新创建的主页。

初始化时间较长的应用必须显示加载页面。 除了提供有关激活过程的用户反馈之外,如果在激活过程开始后的 15 秒内未调用 Window.Activate ,该过程将终止。

partial class GameHomePage : Page
{
    public GameHomePage()
    {
        InitializeComponent();

        // add a handler to be called when the home page has been loaded
        this.Loaded += ReaderHomePageLoaded;

        // load the minimal amount of image and sound data from disk necessary to create the home page.
    }

    void ReaderHomePageLoaded(object sender, RoutedEventArgs e)
    {
        // set the content of the window to the home page now that it's ready to be displayed.
        Window.Current.Content = this;
    }

    // Shown for demonstration purposes only.
    // This is typically autogenerated by Visual Studio.
    private void InitializeComponent()
    {
    }
}
    Partial Friend Class GameHomePage
    Inherits Page

    Public Sub New()
        InitializeComponent()

        ' add a handler to be called when the home page has been loaded
        AddHandler Me.Loaded, AddressOf ReaderHomePageLoaded

        ' load the minimal amount of image and sound data from disk necessary to create the home page.
    End Sub

    Private Sub ReaderHomePageLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
        ' set the content of the window to the home page now that it's ready to be displayed.
        Window.Current.Content = Me
    End Sub

    ' Shown for demonstration purposes only.
    ' This is typically autogenerated by Visual Studio.
    Private Sub InitializeComponent()
    End Sub
End Class

有关使用延长显示的启动画面的示例,请参阅 启动画面示例

第 3 阶段

仅仅因为应用显示 UI 并不意味着它已完全可供使用。 对于我们的游戏,用户界面以占位符形式显示那些需要从互联网上获取数据的功能。 此时,游戏会下载使应用功能完全正常运行所需的附加数据,并在获取数据时逐步启用功能。

有时,激活所需的大部分内容都可以与应用一起打包。 简单游戏就是这样。 这使得激活过程非常简单。 但许多节目(如新闻读者和照片观看者)必须从网上拉取信息才能发挥作用。 此数据可能很大,需要相当多的时间才能下载。 应用在激活过程中如何获取此数据可能会对应用的感知性能产生巨大影响。

如果应用尝试在激活的第一或第二阶段下载整个数据集,可能会显示几分钟的加载页面,甚至更糟的情况下显示初始屏幕。 这会让应用程序看起来像是卡住了,或者导致它被系统终止。 建议应用下载最少的数据量,以在第 2 阶段显示具有占位符元素的交互式 UI,然后逐步加载数据,以替换第 3 阶段中的占位符元素。 有关处理数据的详细信息,请参阅 Optimize ListView 和 GridView

应用在每个启动阶段的反应方式完全由你决定,但应尽可能为用户提供反馈(初始屏幕、加载屏幕、数据加载时的界面),让用户感觉应用和整个系统都很快速。

最小化启动路径中的托管程序集

可重用代码通常以项目中包含的模块(DLL)的形式提供。 加载这些模块需要访问磁盘,正如你想象的那样,这样做的成本可能会增加。 这对冷启动的影响最大,但它也会对热启动产生影响。 对于 C# 和 Visual Basic,CLR 会尝试通过按需加载程序集来尽可能延迟该成本。 也就是说,在执行的方法引用模块之前,CLR 不会加载它。 因此,仅引用启动代码中启动应用所需的程序集,以便 CLR 不会加载不必要的模块。 如果在启动路径中具有不必要的引用的未使用代码路径,则可以将这些代码路径移动到其他方法以避免不必要的加载。

减少模块负载的另一种方法是合并应用模块。 加载一个大型程序集通常比加载两个小程序集所需的时间要短。 这并非总是可能的,仅当模块没有对开发人员工作效率或代码可重用性产生重大影响时,才应该合并模块。 可以使用 PerfViewWindows 性能分析器(WPA) 等工具来了解启动时加载哪些模块。

发出智能 Web 请求

可以通过在本地打包应用内容(包括 XAML、图像和对应用非常重要的任何其他文件)来显著改善应用的加载时间。 磁盘操作比网络操作更快。 如果应用在初始化时需要特定文件,可以通过从磁盘加载它而不是从远程服务器检索它来减少总体启动时间。

高效记录和缓存页面

Frame 控件提供导航功能。 它提供页面导航(Navigate 方法)、导航日记(BackStack/ForwardStack 属性、GoForward/GoBack 方法)、页面缓存(Page.NavigationCacheMode)和序列化支持(GetNavigationState 方法)。

使用 Frame 时要注意的性能主要围绕日记和页面缓存。

帧日记。 使用 Frame.Navigate()导航到页面时,当前页的 PageStackEntry 将添加到 Frame.BackStack 集合。 PageStackEntry 相对较小,但 BackStack 集合的大小没有内置限制。 用户有可能在循环中导航并无限期地增长此集合。

PageStackEntry 还包括传递给 Frame.Navigate() 方法的参数。 建议该参数是基元可序列化类型(如 int 或 string),以便允许 Frame.GetNavigationState() 方法正常工作。 但是,该参数可能会引用一个对象,该对象会占用大量工作集或其他资源,从而使 BackStack 中的每个条目的成本更高。 例如,可以使用 StorageFile 作为参数,因此 BackStack 将无限数量的文件保持打开状态。

因此,建议保持导航参数较小,并限制 BackStack 的大小。 BackStack 是一个标准向量(C# 中的 IList,Platform::Vector in C++/CX),因此只需删除条目即可剪裁。

页面缓存。 默认情况下,使用 Frame.Navigate 方法导航到页面时,将创建页面的新实例。 同样,如果使用 Frame.GoBack 导航回到上一页,则会分配上一页的新实例。

不过,Frame 提供了一个可选的页面缓存,可以避免这些实例化。 若要获取放入缓存中的页面,请使用 Page.NavigationCacheMode 属性。 将该模式设置为“必需”将强制缓存页面,将其设置为“已启用”将允许缓存它。 默认情况下,缓存大小为10页,但可以通过 Frame.CacheSize 属性覆盖。 所有必需页面都将被缓存,如果必需页面数少于 CacheSize,那么已启用的页面也可以被缓存。

页面缓存可以通过避免实例化来帮助性能,从而改进导航性能。 页面缓存可能会因过度缓存而损害性能,从而影响工作集。

因此,建议根据应用程序使用页面缓存。 例如,假设你有一个应用显示 Frame 中的项目列表,点击某个项目时,它会将框架导航到该项目的详细信息页面。 列表页可能设置为缓存。 如果所有项的详细信息页相同,则也可能缓存该页。 但是,如果详细页面更不一致,最好关闭缓存。