分析用于在内存中构造对象的 XAML 标记对于复杂的 UI 来说非常耗时。 这里有一些方法可以改进 XAML 标记的解析和加载时间,以及提升你的应用程序的内存效率。
在应用启动时,将加载的 XAML 标记限制为初始 UI 所需的标记。 检查初始页面中的标记(包括页面资源),并确认你不会立即加载不需要的额外元素。 这些元素可能来自各种来源,例如资源字典、最初折叠的元素以及叠加在其他元素上的元素。
优化 XAML 以提高效率需要权衡;对于每种情况,并不总是有一个解决方案。 在这里,我们将了解一些常见问题,并提供可用于为应用做出适当权衡的指南。
最小化元素计数
尽管 XAML 平台能够显示大量元素,但你可以使用最少数量的元素来实现所需的视觉对象,使应用布局和呈现速度更快。
设置 UI 控件布局时所做的选择会影响应用启动时创建的 UI 元素数。 有关优化布局的更多详细信息,请参阅 “优化 XAML 布局”。
元素计数在数据模板中非常重要,因为为每个数据项再次创建每个元素。 有关减少列表或网格中元素数量的信息,请参阅文章《ListView 和 GridView UI 优化》中的“每个项的元素减少”部分(),位于和节。
在这里,我们将了解一些其他方法,以减少应用在启动时必须加载的元素数。
延迟项目创建
如果 XAML 标记包含未立即显示的元素,则可以延迟加载这些元素,直到显示这些元素。 例如,可以延迟创建不可见的内容,例如类似于选项卡的 UI 中的辅助选项卡。 或者,默认情况下,你可能会在网格视图中显示项,但提供一个选项供用户改为查看列表中的数据。 可以延迟加载列表,直到需要它。
使用 x:Load 属性 而不是 Visibility 属性来控制何时显示元素。 当元素的可见性设置为 折叠时,渲染过程中该元素会被跳过,但在内存中仍然占用对象实例的成本。 改用 x:Load 时,框架在需要对象实例之前不会创建对象实例,因此内存成本甚至更低。 缺点是未加载 UI 时,需要支付少量内存开销(约 600 字节)。
注释
可以使用 x:Load 或 x:DeferLoadStrategy 属性延迟加载元素。 从 Windows 10 创作者更新(版本 1703,SDK 内部版本 15063)开始,可以使用 x:Load 属性。 Visual Studio 项目面向的最小版本必须 Windows 10 创意者更新(10.0,版本号 15063) 才能使用 x:Load。 若要面向早期版本,请使用 x:DeferLoadStrategy 属性。
以下示例显示了在使用不同的技术隐藏 UI 元素时元素计数和内存使用的差异。 ListView 和包含相同项的 GridView 放置在页面的根网格中。 ListView 不可见,但 GridView 可见。 每个示例中的 XAML 都会在屏幕上生成相同的 UI。 我们使用 Visual Studio 的 工具进行分析和性能 来检查元素计数和内存使用情况。
选项 1 - 效率低下
此时,ListView 已加载,但不可见,因为它的宽度为 0。 ListView 及其每个子元素在可视化树中创建并加载到内存中。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE.-->
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView x:Name="List1" Width="0">
<ListViewItem>Item 1</ListViewItem>
<ListViewItem>Item 2</ListViewItem>
<ListViewItem>Item 3</ListViewItem>
<ListViewItem>Item 4</ListViewItem>
<ListViewItem>Item 5</ListViewItem>
<ListViewItem>Item 6</ListViewItem>
<ListViewItem>Item 7</ListViewItem>
<ListViewItem>Item 8</ListViewItem>
<ListViewItem>Item 9</ListViewItem>
<ListViewItem>Item 10</ListViewItem>
</ListView>
<GridView x:Name="Grid1">
<GridViewItem>Item 1</GridViewItem>
<GridViewItem>Item 2</GridViewItem>
<GridViewItem>Item 3</GridViewItem>
<GridViewItem>Item 4</GridViewItem>
<GridViewItem>Item 5</GridViewItem>
<GridViewItem>Item 6</GridViewItem>
<GridViewItem>Item 7</GridViewItem>
<GridViewItem>Item 8</GridViewItem>
<GridViewItem>Item 9</GridViewItem>
<GridViewItem>Item 10</GridViewItem>
</GridView>
</Grid>
加载 ListView 的实时可视化树。 页面的总元素计数为 89。
ListView 及其子组件将加载到内存中。
选项 2 - 更好
在这里,ListView 的可见性设置为折叠(另一个 XAML 与原始 XAML 相同)。 ListView 是在可视化树中创建的,但其子元素不是。 但是,它们被加载到内存中,因此内存使用与前面的示例相同。
<ListView x:Name="List1" Visibility="Collapsed">
实时视觉树,其中 ListView 被折叠。 页面的总元素计数为 46。
ListView 及其子组件将加载到内存中。
选项 3 - 最高效
此处,ListView 的 x:Load 属性设置为 False(其他 XAML 与原始相同)。 ListView 不是在可视化树中创建的,也不会在启动时加载到内存中。
<ListView x:Name="List1" Visibility="Collapsed" x:Load="False">
未加载 ListView 的实时可视化树。 页面的总元素计数为 45。
ListView 及其子级不会加载到内存中。
注释
这些示例中的元素计数和内存使用非常小,只显示用于演示概念。 在这些示例中,使用 x:Load 的开销大于内存节省,因此应用不会受益。 应使用应用中的分析工具来确定应用是否会受益于延迟加载。
使用布局面板属性
布局面板具有 背景 属性,因此无需在面板前面放置 一个矩形 即可对其进行着色。
低 效
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid>
<Rectangle Fill="Black"/>
</Grid>
高效
<Grid Background="Black"/>
布局面板还具有内置边框属性,因此无需在布局面板周围放置 Border 元素。 有关详细信息和示例,请参阅 “优化 XAML 布局 ”。
使用图像代替基于矢量的元素
如果重复使用相同的基于矢量的元素足够多的时间,则改用 Image 元素会更加高效。 基于矢量的元素可能更昂贵,因为 CPU 必须单独创建每个单独的元素。 图像文件只需解码一次。
优化资源和资源字典
通常使用 资源字典 在一些全局级别存储要在应用中多个位置引用的资源。 例如,样式、画笔、模板等。
一般情况下,我们优化了 ResourceDictionary ,以避免在未请求时实例化资源。 但在某些情况下,应避免资源被不必要地实例化。
具有 x:Name 的资源
使用 x:Key 属性 引用您的资源。 具有 x:Name 属性 的任何资源都不会受益于平台优化;而是在创建 ResourceDictionary 后立即实例化它。 发生这种情况是因为 x:Name 指示平台,您的应用需要对该资源进行字段访问,因此平台需要创建一个对象来引用该资源。
UserControl 中的 ResourceDictionary
在 UserControl 内定义的 ResourceDictionary 将受到处罚。 平台为每个 UserControl 实例创建此类 ResourceDictionary 的副本。 在频繁使用 UserControl 的情况下,请将 ResourceDictionary 从 UserControl 中移出,并放置在页面级别。
资源和资源字典范围
如果页面引用了不同文件中定义的用户控件或资源,则框架也会分析该文件。
此处,由于 InitialPage.xaml 使用 ExampleResourceDictionary.xaml 中的一个资源,因此必须在启动时分析 整个 ExampleResourceDictionary.xaml 。
InitialPage.xaml。
<Page x:Class="ExampleNamespace.InitialPage" ...>
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ExampleResourceDictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Page.Resources>
<Grid>
<TextBox Foreground="{StaticResource TextBrush}"/>
</Grid>
</Page>
ExampleResourceDictionary.xaml。
<ResourceDictionary>
<SolidColorBrush x:Key="TextBrush" Color="#FF3F42CC"/>
<!--This ResourceDictionary contains many other resources that
are used in the app, but are not needed during startup.-->
</ResourceDictionary>
如果在应用的许多页面上使用某个资源,那么将其存储在 App.xaml 是一种很好的做法,可以避免重复。 但是 App.xaml 在应用启动时进行解析,因此在某个页面中仅使用的任何资源(除非该页面是初始页面)都应放入该页面的本地资源中。 此示例显示 App.xaml 包含仅由一个页面(不是初始页面)使用的资源。 这不需要增加应用启动时间。
App.xaml
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Application ...>
<Application.Resources>
<SolidColorBrush x:Key="DefaultAppTextBrush" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="InitialPageTextBrush" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="SecondPageTextBrush" Color="#FF3F42CC"/>
<SolidColorBrush x:Key="ThirdPageTextBrush" Color="#FF3F42CC"/>
</Application.Resources>
</Application>
InitialPage.xaml。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page x:Class="ExampleNamespace.InitialPage" ...>
<StackPanel>
<TextBox Foreground="{StaticResource InitialPageTextBrush}"/>
</StackPanel>
</Page>
SecondPage.xaml。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page x:Class="ExampleNamespace.SecondPage" ...>
<StackPanel>
<Button Content="Submit" Foreground="{StaticResource SecondPageTextBrush}"/>
</StackPanel>
</Page>
为了使此示例更高效,请将 InitialPageTextBrush
可以保留在 App.xaml 中,因为在任何情况下都必须在应用启动时分析应用程序资源。
将多个看起来相同的画笔合并到一个资源中
XAML 平台尝试缓存常用对象,以便尽可能频繁地重复使用它们。 但 XAML 无法轻松判断在一个标记中声明的画笔是否与在另一个标记中声明的画笔相同。 此处的示例使用 SolidColorBrush 来演示,但 对于 GradientBrush 而言,这种情况的可能性更大且更重要。 另请检查画笔是否使用预定义颜色;例如,"Orange"
和 "#FFFFA500"
是相同的颜色。
低 效。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page ... >
<StackPanel>
<TextBlock>
<TextBlock.Foreground>
<SolidColorBrush Color="#FFFFA500"/>
</TextBlock.Foreground>
</TextBlock>
<Button Content="Submit">
<Button.Foreground>
<SolidColorBrush Color="#FFFFA500"/>
</Button.Foreground>
</Button>
</StackPanel>
</Page>
若要修复重复,请将画笔定义为资源。 如果其他页面中的控件使用相同的画笔,请将其移动至 App.xaml。
有效。
<Page ... >
<Page.Resources>
<SolidColorBrush x:Key="BrandBrush" Color="#FFFFA500"/>
</Page.Resources>
<StackPanel>
<TextBlock Foreground="{StaticResource BrandBrush}" />
<Button Content="Submit" Foreground="{StaticResource BrandBrush}" />
</StackPanel>
</Page>
最小化过度绘制
过度绘制是指多个对象被绘制在同一屏幕像素上。 请注意,本指南有时与最小化元素计数的愿望之间存在权衡。
使用 DebugSettings.IsOverdrawHeatMapEnabled 作为视觉诊断工具。 你可能会发现一些你不知道存在于场景中的对象被绘制出来。
透明或隐藏元素
如果某个元素不可见,因为它在其他元素后面是透明或隐藏的,并且它不导致布局,请将其删除。 如果元素在初始视觉状态中不可见,但在其他视觉状态中可见,那么可以使用 x:Load 来控制其状态,或者在元素本身上将 Visibility 设置为 Collapsed,并在适当的状态下将值更改为 Visible。 这种启发式方法会有例外情况:一般来说,属性在大多数视觉状态中的值最好在元素上本地设置。
复合元素
使用复合元素而不是分层多个元素来创建效果。 在此示例中,结果是一个双色形状,上半部分为黑色(来自 网格的背景),下半部分为灰色(来自半透明白色 矩形 和 网格的黑色背景之间的 Alpha 混合效果)。 在这里,正在填充实现结果所需的 150 个% 像素。
低 效。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="1" Fill="White" Opacity=".5"/>
</Grid>
有效。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Black"/>
<Rectangle Grid.Row="1" Fill="#FF7F7F7F"/>
</Grid>
布局面板
布局面板可以有两个用途:为区域着色和布局子元素。 如果在 z 顺序中更靠后的元素已经为区域渲染颜色,那么在前面的布局面板就不需要再绘制该区域:它可以只专注于安排它的子元素。 下面是一个示例。
低 效。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<GridView Background="Blue">
<GridView.ItemTemplate>
<DataTemplate>
<Grid Background="Blue"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
有效。
<GridView Background="Blue">
<GridView.ItemTemplate>
<DataTemplate>
<Grid/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
如果 网格 必须具有可命中测试的属性,则为其设置透明的背景值。
边界
使用 Border 元素在对象周围绘制边框。 在此示例中, 网格 用作 TextBox 周围的临时边框。 但中心单元格中的所有像素都是过度绘制的。
低 效。
<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid Background="Blue" Width="300" Height="45">
<Grid.RowDefinitions>
<RowDefinition Height="5"/>
<RowDefinition/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Row="1" Grid.Column="1"></TextBox>
</Grid>
有效。
<Border BorderBrush="Blue" BorderThickness="5" Width="300" Height="45">
<TextBox/>
</Border>
页边距
请注意边距。 如果负边距扩展到另一个元素的呈现边界并导致重绘过多,两个相邻的元素可能会(意外地)重叠。
缓存静态内容
过度绘制的另一个可能来源是由许多重叠元素构成的形状。 如果在包含复合形状的 UIElement 上,将 CacheMode 设置为 BitmapCache,那么平台将元素呈现为位图一次,然后在每一帧中使用该位图,而不是重复绘制。
低 效。
<Canvas Background="White">
<Ellipse Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Left="21" Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Top="13" Canvas.Left="10" Height="40" Width="40" Fill="Blue"/>
</Canvas>
上图是结果,但下面是透支区域的地图。 深红色表示过度绘制量较高。
有效。
<Canvas Background="White" CacheMode="BitmapCache">
<Ellipse Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Left="21" Height="40" Width="40" Fill="Blue"/>
<Ellipse Canvas.Top="13" Canvas.Left="10" Height="40" Width="40" Fill="Blue"/>
</Canvas>
请注意使用 CacheMode。 如果任何子形状动画化,请不要使用此技术,因为位图缓存可能需要重新生成每个帧,从而破坏目的。
使用 XBF2
XBF2 是 XAML 标记的二进制表示形式,可避免在运行时产生所有文本分析成本。 它还优化了用于加载和树结构创建的二进制文件,并为 XAML 类型提供“快速路径”,以改进堆和对象创建成本,例如 VSM、ResourceDictionary、Styles 等。 它是完全内存映射的,因此在加载和读取 XAML 页面时,没有堆内存消耗。 此外,它减少了 appx 中存储的 XAML 页面的磁盘占用。 XBF2 是一种更精简的表示形式,它可以将比较 XAML/XBF1 文件的磁盘占用量减少多达 50%。 例如,在转换为 XBF2 后,内置照片应用大约减少了 60%,从大约 1mb 的 XBF1 资产下降到大约 400kb 的 XBF2 资产。 我们还观察到,应用程序在 CPU 中的% 受益范围为 15 到 20,Win32 堆中的% 受益范围为 10 到 15。
框架提供的 XAML 内置控件和字典已经完全启用了 XBF2。 对于你自己的应用,请确保项目文件声明 TargetPlatformVersion 8.2 或更高版本。
若要检查是否具有 XBF2,请在二进制编辑器中打开应用;如果有 XBF2,则第 12 个字节和第 13 个字节为 00 02。