应用分析概述

应用分析是一种工具,可向开发人员提供性能问题的提示通知。 应用分析根据一组性能准则和最佳做法运行应用代码。

应用分析从一组常见性能问题的规则集中识别出应用的问题。 适当时,应用分析将指向 Visual Studio 的时间线工具、源信息和文档,为你提供调查方法。

应用分析中的规则是指要检查应用的准则或最佳做法。

解码的图像大小大于呈现大小

图像以非常高的分辨率捕获,这可能会导致应用在从磁盘加载映像数据时使用更多的 CPU 和更多内存。 但是,没有必要在内存中解码和保存高分辨率图像,只是为了将其显示得比原始大小更小。 相反,请使用 DecodePixelWidth 和 DecodePixelHeight 属性创建与将在屏幕上绘制的图像尺寸完全相同的图像版本。

影响

在非原始大小上显示图像可能会对 CPU 使用时间(由于解码为适当大小)和内存产生负面影响,并延长下载时间。

原因和解决方案

图像未被异步设置

应用使用 SetSource()而不是 SetSourceAsync()。 应始终避免使用 SetSource,而是在将流设置为异步解码图像时使用 SetSourceAsync

在 ImageSource 不在实时树中时调用图像

使用 SetSourceAsync 或 UriSource 设置内容后,BitmapImage 连接到实时 XAML 树。 设置源之前,应始终将 BitmapImage 附加到实时树中。 每当在标记中指定图像元素或画笔时,这将自动成为这种情况。 下面提供了示例。

实时树示例

示例 1 (良好)—— 标记中指定的统一资源标识符(URI)。

<Image x:Name="myImage" UriSource="Assets/cool-image.png"/>

示例 2 标记 - 代码隐藏中指定的 URI。

<Image x:Name="myImage"/>

示例 2 代码隐藏(良好)— 在设置其 UriSource 之前将 BitmapImage 连接到树。

var bitmapImage = new BitmapImage();
myImage.Source = bitmapImage;
bitmapImage.UriSource = new URI("ms-appx:///Assets/cool-image.png", UriKind.RelativeOrAbsolute);

示例 2 的后台代码(错误)—在将 BitmapImage 连接到树之前设置其 UriSource。

var bitmapImage = new BitmapImage();
bitmapImage.UriSource = new URI("ms-appx:///Assets/cool-image.png", UriKind.RelativeOrAbsolute);
myImage.Source = bitmapImage;

图像画笔不是矩形的

当图像用于非矩形画笔时,图像将使用软件光栅化路径,这根本不会缩放图像。 此外,它必须在软件和硬件内存中存储映像的副本。 例如,如果将图像用作椭圆的画笔,可能的大型完整图像将在内部被存储两次。 使用非矩形画笔时,应用程序应将其图像预先缩放到接近实际渲染大小。

或者,可以设置显式解码大小,以便在屏幕上按照确切大小绘制图像的版本,使用 DecodePixelWidthDecodePixelHeight 属性。

<Image>
    <Image.Source>
    <BitmapImage UriSource="ms-appx:///Assets/highresCar.jpg" 
                 DecodePixelWidth="300" DecodePixelHeight="200"/>
    </Image.Source>
</Image>

DecodePixelWidthDecodePixelHeight 的单位默认为物理像素。 DecodePixelType 属性可用于更改这一行为:将 DecodePixelType 设置为 逻辑 后,解码大小会自动根据系统当前的比例因子进行调整,这与其他 XAML 内容的处理方式类似。 因此,如果希望 DecodePixelWidthDecodePixelHeight 来匹配将显示图片的图像控件的高度和宽度属性,则通常适合将 DecodePixelType 设置为 逻辑。 使用物理像素的默认行为时,您需要自行考虑系统的当前缩放比例,以防用户更改其显示首选项,并且应侦听缩放更改通知。

在某些情况下,当无法提前确定适当的解码尺寸时,您应该依赖于 XAML 的自动合适尺寸解码。如果未指定解码像素宽度或高度,XAML 将尽力尝试以合适的尺寸解码图像。

如果提前知道图像内容的大小,则应设置显式解码大小。 如果提供的解码大小是相对于其他 XAML 元素大小的,那么您还应将 DecodePixelType 设置为 Logical。 例如,如果使用 Image.Width 和 Image.Height 显式设置内容大小,则可以将 DecodePixelType 设置为 DecodePixelType.Logical 以使用与图像控件相同的逻辑像素维度,然后显式使用 BitmapImage.DecodePixelWidth 和/或 BitmapImage.DecodePixelHeight 来控制图像的大小,以实现潜在的大内存节省。

请注意,在确定解码内容大小时,需要考虑 Image.Stretch。

BitmapIcons 内使用的图像回退到解码为自然大小

设置显式解码大小,以通过使用 DecodePixelWidthDecodePixelHeight 属性来创建图像在屏幕上显示时的确切大小。

屏幕上出现非常大的图像回退到解码为自然大小

屏幕上出现非常大的图像会回退到解码为自然大小。 设置显式解码大小,以通过使用 DecodePixelWidthDecodePixelHeight 属性来创建图像在屏幕上显示时的确切大小。

图像已隐藏

该图像通过将不透明度设置为 0 或可见性设置为折叠在主机图像元素或画笔或任何父元素上隐藏。 由于剪辑或透明度而在屏幕上不可见的图像可能会回退到解码为自然大小。

图像正在使用 NineGrid 属性

当图像用于 NineGrid时,图像将使用软件的光栅化路径,这意味着图像不会被缩放。 此外,它必须在软件和硬件内存中存储映像的副本。 使用 NineGrid时,你的应用应将其图像预先缩放到与呈现时大小相近的尺寸。

使用 NineGrid 属性的图像回退到自然大小解码。 请考虑将 Ninegrid 效果添加到原始图像。

DecodePixelWidth 或 DecodePixelHeight 被设置为大于图像在屏幕上显示的大小。

如果 DecodePixelWidth/Height 显式设置为大于屏幕上显示的图像,则应用将不必要地使用额外的内存,每个像素最多 4 个字节,这对于大型图像来说很快变得昂贵。 图像还将使用双线性缩放进行缩小,这可能导致图像在较大的缩放因子上显得模糊。

图像作为生成拖放图像的一部分进行解码

设置显式解码大小,以通过使用 DecodePixelWidthDecodePixelHeight 属性来创建图像在屏幕上显示时的确切大小。

加载时折叠的元素

应用中的常见模式是最初隐藏 UI 中的元素,并在以后显示它们。 在大多数情况下,应使用 x:Load 或 x:DeferLoadStrategy 延迟这些元素,以避免在加载时创建元素的成本。

这包括一个布尔值到可见性转换器用于隐藏项直到以后为止的情况。

影响

折叠元素与其他元素一起加载,并有助于增加加载时间。

原因

由于元素在加载时已折叠,因此触发了此规则。 折叠元素或将其不透明度设置为 0 不会阻止创建元素。 此规则可能是由使用布尔值到可见性转换器的应用引起的,该转换器默认为 false。

解决方案

使用 x:Load 属性x:DeferLoadStrategy,可以延迟加载一段 UI,并在需要时加载它。 这是推迟处理在第一帧中不可见的 UI 的好方法。 可以选择在需要时或作为一组延迟逻辑的一部分加载元素。 若要触发加载,请对要加载的元素调用 findName。 x:Load 扩展了 x:DeferLoadStrategy 的功能,使元素能够卸载,并通过 x:Bind 控制加载状态。

在某些情况下,使用 findName 显示一段 UI 可能不是答案。 如果您希望在单击按钮时实现显著的一部分用户界面并保持非常低的延迟,那么这是事实。 在这种情况下,你可能希望通过消耗额外的内存来换取更快的界面响应速度。如果是这样,你应该使用 x:DeferLoadStrategy 并将要实现的元素的 Visibility 设置为 Collapsed。 加载页面并释放 UI 线程后,可以在必要时调用 findName 来加载元素。 在将元素的 Visibility 设置为 Visible 之前,元素不会对用户可见。

ListView 未虚拟化

UI 虚拟化是为了提高收集性能而做出的最重要改进。 这意味着,表示项的 UI 元素是按需创建的。 对于绑定到 1000 项集合的项控件,将浪费资源来同时为所有项创建 UI,因为它们不能同时显示。 ListView 和 GridView(以及其他标准 ItemsControl 派生控件)为你执行 UI 虚拟化。 当项目即将滚动进入视图(距离视图还有几页)时,框架会生成项目的用户界面并缓存它们。 如果某些项目不太可能再次显示,框架将回收内存。

UI 虚拟化只是提高收集性能的几个关键因素之一。 减少收集项和数据虚拟化的复杂性是提高收集性能的另外两个重要方面。 有关提高 ListViews 和 GridViews 中的集合性能的详细信息,请参阅有关 ListView 和 GridView UI 优化 以及 ListView 和 Gridview 数据虚拟化的文章。

影响

非虚拟化的 ItemsControl 会通过加载超出必要数量的子项而增加加载时间和资源使用率。

原因

视区的概念对于 UI 虚拟化至关重要,因为框架必须创建可能显示的元素。 通常,ItemsControl 的视区是逻辑控件的范围。 例如,ListView 的视区是 ListView 元素的宽度和高度。 某些面板允许子元素无限空间,例如 ScrollViewer 和 Grid,以及自动调整大小的行或列。 将虚拟化 ItemsControl 放置在这样的面板中时,它需要足够的空间来显示其所有项,这会使虚拟化失败。

解决方案

通过在正在使用的 ItemsControl 上设置宽度和高度来还原虚拟化。

加载期间 UI 线程被阻塞或空闲

UI 线程阻塞是指对运行在非 UI 线程上的函数进行的同步调用,这些调用会阻塞 UI 线程。

有关改进应用的启动性能的最佳做法的完整列表,请参阅 应用的启动性能最佳做法保持 UI 线程响应

影响

加载期间被阻止或空闲的 UI 线程将阻止布局和其他 UI 操作,从而增加启动时间。

原因

UI 的平台代码和 UI 的应用代码都在同一 UI 线程上执行。 一次只能执行一个指令,因此,如果应用代码处理事件需要太长的时间,框架将无法运行布局或引发表示用户交互的新事件。 应用的响应能力与处理工作的 UI 线程的可用性相关。

解决方案

即使应用的某些部分功能不完全正常运行,应用也可以是交互式应用。 例如,如果应用显示检索需要一段时间的数据,则可以通过异步检索数据使该代码独立于应用的启动代码执行。 当数据可用时,使用数据填充应用的用户界面。 为了帮助你的应用保持响应,该平台提供了许多 API 的异步版本。 异步 API 可确保您的活动执行线程不会被长时间阻塞。 从 UI 线程调用 API 时,请使用异步版本(如果可用)。

正在使用 {Binding} 而不是 {x:Bind}

当应用使用 {Binding} 语句时,将触发此规则。 为了提高应用性能,应用应考虑使用 {x:Bind}。

影响

{Binding} 运行时占用的时间和内存比 {x:Bind} 更多。

原因

应用使用 {Binding} 而不是 {x:Bind}。 {Binding} 带来了非普通的工作集和 CPU 开销。 创建 {Binding} 会导致一系列的分配,而更新绑定目标可能会引发反射和装箱。

解决方案

使用 {x:Bind} 标记扩展,该扩展在生成时编译绑定。 {x:Bind} 绑定(通常称为编译绑定)具有卓越的性能,提供对绑定表达式的编译时验证,并支持调试,您可以在为页面生成的分部类的代码文件中设置断点以进行调试。

请注意,x:Bind 不适用于所有情况,例如后期绑定场景。 有关 {x:Bind} 未涵盖的案例的完整列表,请参阅 {x:Bind} 文档。

使用的是 x:Name 而不是 x:Key

ResourceDictionaries 通常用于以某种全局级别存储资源,即应用希望在多个位置引用的资源;例如,样式、画笔、模板等。 一般情况下,我们优化了 ResourceDictionaries,以不实例化资源,除非要求它们。 但有几个地方需要稍微注意一下。

影响

创建 ResourceDictionary 后,将立即实例化具有 x:Name 的任何资源。 发生这种情况是因为 x:Name 指示平台,您的应用需要对该资源进行字段访问,因此平台需要创建一个对象来引用该资源。

原因

你的应用正在对资源设置 x:Name。

解决方案

如果不从后台代码引用资源,请使用 x:Key 而不是 x:Name。

集合控件正在使用非虚拟化面板

如果提供自定义项面板模板(请参阅 ItemsPanel),请确保使用虚拟化面板,如 ItemsWrapGrid 或 ItemsStackPanel。 如果使用 VariableSizedWrapGrid、WrapGrid 或 StackPanel,则不会获得虚拟化。 此外,仅在使用 ItemsWrapGrid 或 ItemsStackPanel 时,才会引发以下 ListView 事件:ChoosingGroupHeaderContainer、ChoosingItemContainer 和 ContainerContentChanging。

UI 虚拟化是为了提高收集性能而做出的最重要改进。 这意味着,表示项的 UI 元素是按需创建的。 对于绑定到 1000 项集合的项控件,将浪费资源来同时为所有项创建 UI,因为它们不能同时显示。 ListView 和 GridView(以及其他标准 ItemsControl 派生控件)为你执行 UI 虚拟化。 当项目即将滚动进入视图(距离视图还有几页)时,框架会生成项目的用户界面并缓存它们。 如果某些项目不太可能再次显示,框架将回收内存。

UI 虚拟化只是提高收集性能的几个关键因素之一。 减少收集项和数据虚拟化的复杂性是提高收集性能的另外两个重要方面。 有关提高 ListViews 和 GridViews 中的集合性能的详细信息,请参阅有关 ListView 和 GridView UI 优化 以及 ListView 和 Gridview 数据虚拟化的文章。

影响

非虚拟化的 ItemsControl 会通过加载超出必要数量的子项而增加加载时间和资源使用率。

原因

你正在使用不支持虚拟化的面板。

解决方案

使用虚拟化面板,如 ItemsWrapGrid 或 ItemsStackPanel。

辅助功能:没有名称的 UIA 元素

在 XAML 中,可以通过设置 AutomationProperties.Name 来提供名称。 如果 AutomationProperties.Name 未设置,许多自动化对等方为 UIA 提供默认名称。

影响

如果用户到达没有名称的元素,他们通常无法知道该元素与什么相关。

原因

元素的 UIA 名称为 null 或为空。 此规则检查 UIA 看到的内容,而不是 AutomationProperties.Name 的值。

解决方案

将控件 XAML 中的 AutomationProperties.Name 属性设置为适当的本地化字符串。

有时正确的应用程序修复不是去提供名称,而是将 UIA 元素从除基础树外的所有树中删除。 可以通过设置 AutomationProperties.AccessibilityView = "Raw"在 XAML 中执行此操作。

辅助功能:相同控件类型的 UIA 元素不应有相同的名称

具有相同 UIA 父级的两个 UIA 元素不能具有相同的名称和控件类型。 如果两个控件的控制类型不同,那么它们可以具有相同的名称。

此规则不会检查具有不同父级的重复名称。 但是,在大多数情况下,即使具有不同的父级,也不应在整个窗口内重复 名称 和 控件类型。 窗口内重复名称可接受的情况是包含相同项的两个列表。 在这种情况下,列表项应具有相同的名称和控件类型。

影响

如果用户到达一个与另一个具有相同 UIA 父级的元素拥有相同名称和控件类型的元素,用户可能无法区分这些元素。

原因

具有相同 UIA 父级的 UIA 元素共享相同的名称和 ControlType。

解决方案

使用 AutomationProperties.Name 在 XAML 中设置名称。 在这种情况常见的列表中,使用绑定功能将 AutomationProperties.Name 的值绑定到数据源。