优化动画、媒体和图像

创建具有平滑动画、高帧速率和高性能媒体捕获和播放的 WinUI 应用。

使动画流畅

WinUI 应用的关键方面是流畅的交互。 这包括“粘住你的手指”的触摸操纵、流畅的过渡和动画,以及提供输入反馈的小动画。 在 XAML 框架中,有一个名为合成线程的线程,专用于应用的视觉元素的合成和动画。 由于合成线程独立于 UI 线程(运行框架和开发人员代码的线程),因此应用可以实现一致的帧速率和平滑动画,而不考虑复杂的布局传递或扩展计算。 本部分介绍如何使用合成线程来保持应用的动画流畅。 有关动画的详细信息,请参阅 动画概述。 若要了解如何在执行密集型计算时提高应用的响应能力,请参阅保持 UI 线程响应。

使用独立动画而不是依赖动画

可以在创建时从头到尾计算独立动画,因为对正在动画处理的属性所做的更改不会影响场景中的其余对象。 因此,独立动画可以在合成线程而不是 UI 线程上运行。 这可以保证它们保持流畅,因为合成线程以一致的节奏进行更新。

所有这些类型的动画都保证是独立的:

依赖动画会影响布局,因此,如果没有 UI 线程的额外输入,则无法计算布局。 依赖动画包括对 WidthHeight 等属性的修改。 默认情况下,依赖动画不会运行,需要应用开发人员的启用。 启用后,如果 UI 线程保持未阻止状态,则它们会顺利运行,但如果框架或应用在 UI 线程上执行了大量其他工作,它们就会开始停滞不前。

默认情况下,XAML 框架中的所有动画都是独立的,但可以采取一些操作来禁用此优化。 特别要注意以下情况:

  • 设置 EnableDependentAnimation 属性以允许依赖动画在 UI 线程上运行。 将这些动画转换为独立版本。 例如,对 ScaleTransform.ScaleXScaleTransform.ScaleY 进行动画处理,而不是对象的 WidthHeight 。 不要害怕缩放图像和文本等对象。 仅当 ScaleTransform 正在进行动画处理时,框架才应用双线性缩放。 图像/文本将在最终尺寸情况下重新栅格化,以确保图像/文本始终清晰。
  • 进行逐帧更新,这实际上是依赖动画。 例如,在 CompositionTarget.Rendering 事件的处理程序中应用转换。
  • 在将 CacheMode 属性设置为 BitmapCache 的元素中运行任何被视为独立的动画。 此类动画可视为从属动画,因为必须为每个帧对缓存重新进行光栅化。

不要对 WebView2 或 MediaPlayerElement 进行动画处理

在 WebView2 控件中托管的 Web 内容不是由 XAML 框架直接呈现的,因此,使用场景的其余部分进行组合需要额外的工作。 当你在屏幕上为控件设置动画时,开销会累积,并可能引入同步问题,例如,如果 Web 内容看起来与周围的 XAML 不同步移动。 如果你需要 Web 内容周围的运动,请为周围的 WinUI 镶边设置动画,而不是 WebView2 表面本身。 前面的指南可能提及 WebViewBrush;在新式 WinUI 应用中,WebView2 是受支持的 Web 托管控件,并且没有直接的 WebViewBrush 替换。

MediaPlayerElement 进行动画处理是一个同样糟糕的想法。 除了有损性能,它还可能导致要播放的视频内容中出现断裂或其他痕迹。

谨慎使用无限动画

大多数动画在指定的时间内执行,但将 Timeline.Duration 属性设置为 Forever 允许无限期运行动画。 建议最大程度地减少无限动画的使用,因为它们会持续消耗 CPU 资源,并可能阻止 CPU 进入低功率或空闲状态,从而导致其更快地耗尽电源。

CompositionTarget.Rendering 添加处理程序类似于运行无限动画。 通常,UI 线程仅在有工作要做时处于活动状态,但为此事件添加处理程序会强制它运行每个帧。 当没有工作要做时删除处理程序,并在再次需要时重新注册它。

使用动画库

Microsoft.UI.Xaml.Media.Animation 命名空间包含一个高性能、流畅的动画库,这些动画的外观与其他 Windows 动画保持一致。 相关类的名称为“主题”,并在 动画概述中介绍。 此库支持许多常见的动画方案,例如对应用的第一个视图进行动画处理以及创建状态和内容转换。 我们建议尽可能使用此动画库来提高 WinUI 应用的性能和一致性。

注意 动画库无法对所有可能的属性进行动画处理。 对于动画库不适用的 XAML 场景,请参阅 Storyboard 动画

独立对 CompositeTransform3D 属性进行动画处理

可以独立对 CompositeTransform3D 的每个属性进行动画处理,因此请仅应用所需的动画。 有关示例和详细信息,请参阅 UIElement.Transform3D。 有关动画转换的详细信息,请参阅 情节提要动画关键帧和缓动函数动画

优化媒体资源

音频、视频和图像是大多数应用使用的令人信服的内容形式。 随着媒体捕获速率的提高和内容从标准定义移动到高清,存储、解码和播放此内容所需的资源量将增加。 XAML 框架基于新式 Windows 媒体基础结构构建,因此 WinUI 应用会自动继承其中许多改进。 下面是一些其他技巧,可帮助你充分利用 WinUI 应用中的媒体。

释放媒体流

媒体文件是应用程序通常使用的最常见且昂贵的资源之一。 因为媒体文件资源可大幅增加应用的内存占用的大小,所以你必须记住在应用结束使用媒体文件时立即释放对媒体的处理。

例如,如果应用正在使用 RandomAccessStreamIInputStream 对象,请确保在应用使用完该对象后对对象调用 close 方法,以释放基础对象。

尽可能显示全屏视频播放

在 WinUI 应用中,始终使用 MediaPlayerElement 上的 IsFullWindow 属性来启用和禁用全屏呈现。 这可确保在媒体播放期间使用系统级优化。

XAML 框架可以在仅呈现视频内容时优化其显示,从而减少功耗并提高帧速率,提升用户体验。 对于最有效的媒体播放,请将 MediaPlayerElement 的大小设置为屏幕的宽度和高度,并且不显示其他 XAML 元素。

MediaPlayerElement 上覆盖 XAML 元素有合法的理由,这些元素占据屏幕的完整宽度和高度,例如隐藏式字幕或瞬间传输控件。 确保在不需要这些元素来使媒体播放返回到其最有效的状态时隐藏它们(设置 Visibility="Collapsed")。

显示停用和节省电源

若要防止在不再检测到用户操作时停用显示,例如当应用正在播放视频时,可以调用 DisplayRequest.RequestActive

若要节省电源和电池使用时间,应调用 DisplayRequest.RequestRelease ,在不再需要显示请求后立即释放显示请求。

在以下情况下应该释放显示请求:

  • 例如,由于用户操作、缓冲或有限带宽引起的调整需要暂停视频播放。
  • 播放停止。 例如,视频播放完毕或完成演示文稿。
  • 出现播放错误。 例如,存在网络连接问题或损坏的文件。

将其他元素置于嵌入视频的一侧

通常,应用提供嵌入视图,其中视频在页面中播放。 现在你显然失去了全屏优化,因为 MediaPlayerElement 不是页面的大小,并且绘制了其他 XAML 对象。 请留意,不要无意间通过在 MediaPlayerElement 周围绘制边框而进入这个模式。

请勿在视频处于嵌入模式时在其顶部绘制 XAML 元素。 如果绘制了,则会强制框架执行少量额外的工作来组合场景。 将传输控件放置在嵌入式媒体元素下面,而不是放在视频顶部是针对这种情况进行优化的好示例。 在此图像中,红色条指示一组传输控件(播放、暂停、停止等)。

具有覆盖元素的 MediaPlayerElement

不要将这些控件放在未全屏的媒体上。 而是将传输控件放置在呈现媒体的区域之外的某个位置。 在下一个图像中,控件放置在媒体下方。

具有相邻元素的 MediaPlayerElement

延迟设置 MediaPlayerElement 的源

媒体引擎是昂贵的对象,XAML 框架会延迟加载 DLL 并尽可能创建大型对象。 MediaPlayerElement 在通过 Source 属性设置源后强制执行此操作。 如果用户已准备好播放媒体,则设置此设置会尽可能长时间地延迟与 MediaPlayerElement 关联的大部分成本。

设置 MediaPlayerElement.PosterSource

设置 MediaPlayerElement.PosterSource 可使 XAML 释放本来会使用的某些 GPU 资源。 此 API 允许应用尽可能少地使用内存。

改进媒体清理

要使媒体平台做出真正的响应,清理一直是一项艰巨的任务。 通常,人们可以通过更改滑块的值来实现此目的。 下面是有关如何使此操作尽可能高效的几个提示:

  • 基于查询 MediaPlayerElement.MediaPlayer 上的 Position 的计时器更新 Slider 的值。 请确保对计时器使用合理的更新频率。 Position 属性仅在播放过程中每 250 毫秒更新一次。
  • 滑块上的步进频率的大小必须随视频的长度进行缩放。
  • 订阅滑块上的 PointerPressedPointerMovedPointerReleased 事件,以便当用户拖动滑块的拇指时将 PlaybackRate 属性设置为 0。
  • PointerReleased 事件处理程序中,手动将媒体位置设置为滑块位置值以在清理时实现最佳的缩略图对齐。

将视频分辨率与设备分辨率匹配

解码视频需要大量的内存和 GPU 周期,因此请选择靠近其分辨率的视频格式。 如果要将视频缩小到更小的尺寸,则使用资源来解码1080视频没有意义。 许多应用没有以不同分辨率编码的相同视频;但如果可用,请使用接近其显示分辨率的编码。

媒体格式选择可以是敏感主题,通常由业务决策驱动。 从 Windows 应用 SDK 性能的角度来看,我们建议将 H.264 视频作为主要视频格式,AAC 和 MP3 作为首选音频格式。 对于本地文件播放,MP4 是视频内容的首选文件容器。 H.264 解码通过最新的图形硬件加速。 此外,尽管 VC-1 解码的硬件加速已广泛提供,但对于市场上的大量图形硬件,在许多情况下,加速仅限于部分加速级别(或 IDCT 级别),而不是全流硬件卸载(即 VLD 模式)。

如果完全控制了视频内容生成过程,则必须了解如何在压缩效率和 GOP 结构之间保持良好的平衡。 使用 B 图片的相对更小的 GOP 大小可改进搜寻或诀窍模式的性能。

如果包括短、低延迟的音频效果(例如在游戏中),请使用带有未压缩 PCM 数据的 WAV 文件,以减少压缩音频格式的典型处理开销。

优化图像资源

将图像缩放为适当的大小

图像以非常高的分辨率捕获,这可能会导致应用在从磁盘加载映像数据时使用更多的 CPU 和更多内存。 但是在内存中解码和保存高分辨率图像没有意义,只是将其显示为比其本机大小更小。 相反,应通过 DecodePixelWidthDecodePixelHeight 属性创建一个在屏幕上绘制的图像的精确尺寸的版本。

别这样:

<Image Source="ms-appx:///Assets/highresCar.jpg"
       Width="300" Height="200"/>    <!-- BAD CODE DO NOT USE.-->

请改为执行以下操作:

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

DecodePixelWidthDecodePixelHeight 的单位默认为物理像素。 DecodePixelType 属性可用于更改此行为:将 DecodePixelType 设置为逻辑结果时,解码大小会自动考虑到系统的当前缩放系数,类似于其他 XAML 内容。 因此,如果希望 DecodePixelWidthDecodePixelHeight 匹配图像控件的高度和宽度属性,则通常适合将 DecodePixelType 设置为 Logical。 在使用默认的物理像素行为时,您必须自行考虑系统的当前比例系数;如果用户更改了显示设置,您还应监听缩放更改通知。

如果显式将 DecodePixelWidth/Height 设置得比图像在屏幕上的显示尺寸更大,应用将不必要地使用额外的内存(每个像素最多 4 个字节),这会导致内存使用过高,尤其是对于大型图像而言,代价很高。 图像还将使用双线性缩放进行缩小,这可能导致图像在大比例因子下显得模糊。

如果将 DecodePixelWidth/DecodePixelHeight 显式设置为小于图像的屏幕显示尺寸,图像将被放大,进而可能显得像素化。

在某些情况下,如果无法提前确定合适的解码大小,建议使用 XAML 的自动大小解码功能。在未明确指定 DecodePixelWidth/DecodePixelHeight 时,它会尽可能尝试以适当的大小解码图像。

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

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

正确大小的解码

如果未设置显式解码大小,XAML 会尽力通过将图像解码为屏幕中显示的确切大小(根据包含页面的初始布局)来尽力保存内存。 建议尽可能使用此功能的方式编写应用程序。 如果满足以下任一条件,将禁用此功能。

  • 使用 SetSourceAsyncUriSource 设置内容后,BitmapImage 连接到实时 XAML 树。
  • 图像是使用同步解码(如 SetSource)解码的。
  • 该图像通过将 不透明度 设置为 0 或可见性 设置为 折叠 在主机图像元素或画笔或任何父元素上隐藏。
  • 图像控件或画笔使用 NoneStretch
  • 该图像用作 NineGrid
  • CacheMode="BitmapCache" 在图像元素或任何父元素上设置。
  • 图像画笔是非矩形(例如应用于形状或文本时)。

在上述方案中,设置显式解码大小是实现内存节省的唯一方法。

设置源之前,应始终将 BitmapImage 附加到实时树。 每当在标记中指定图像元素或画笔时,这将自动成为这种情况。 下面提供了“实时树示例”标题下的示例。 应始终避免使用 SetSource ,而是在设置流源时改用 SetSourceAsync 。 在等待触发ImageOpened事件时,最好避免隐藏图像内容(例如将不透明度设为零或折叠可见性)。 主观判断是否执行此操作:如果执行,你将不会从自动正确大小解码中获益。 如果应用最初必须隐藏图像内容,则它还应尽可能显式设置解码大小。

实时树示例

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

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

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

<Image x:Name="myImage"/>

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

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;

缓存优化

对于使用 UriSource 从应用包或网络加载内容的图像,可提供缓存优化。 WinUI 可以在不同的 BitmapImage 实例和 图像 控件中重复使用同一解析 URI 的内部图像数据和呈现资源。 WinUI 缓存这些内部资源,而不是公共对象本身。

当兼容的资源仍然可用时,缓存命中可以避免再次检索或解码映像。 如果同一 URI 需要不同的物理解码维度,WinUI 可能会为这些维度创建另一个解码版本,同时重用其他可用数据。 不能保证重用,因此在导航后返回到 URI 可能需要另一次检索或解码。

当映像具有 URI 并且希望 WinUI 将重复加载标识为同一资源时,请使用 UriSource 。 使用 SetSourceAsync 从不同的流初始化的图像不具有共享的 URI 标识,因此 WinUI 会分别解码每个流。

对于远程 URI,BitmapImage 不确定是否使用内存或磁盘缓存、发送条件请求或从源服务器下载内容。 URI 处理程序、网络堆栈、服务器响应和缓存策略确定该行为。

WinUI 不会公开映像缓存的生存时间、最大大小、逐出顺序或清除操作。 缓存生存期不能保证与应用生存期或特定控件的生存期匹配。 从可视化树中删除 图像 并释放对其 BitmapImage 的引用时,关联的资源有资格发布,但 WinUI 无法完全保证何时返回内存(例如,在删除后不久释放硬件图面,而缓存的图像数据可能保留更长时间)。 保留 BitmapImage 实例可以保留关联的图像资源。

不同的解码维度可能需要单独的解码资源并增加内存使用。 若要减少内存使用, 请将映像缩放为适当的大小 ,并为映像集合使用 虚拟化面板

在同一 URI 上刷新映像

如果 URI 中的内容可以更改,请在设置 UriSource 之前将 CreateOptions 设置为 BitmapCreateOptions.IgnoreImageCache。 此设置会绕过该负载的现有 WinUI 映像缓存条目,并导致 WinUI 再次解码检索的内容。

using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;

private static void RefreshImage(Image image, Uri uri)
{
    var bitmapImage = new BitmapImage
    {
        CreateOptions = BitmapCreateOptions.IgnoreImageCache
    };

    image.Source = bitmapImage;
    bitmapImage.UriSource = uri;
}

IgnoreImageCache 控制 WinUI 的映像缓存,而不是检索远程资源所涉及的每个缓存。 不要依赖此选项来保证每个负载都与源服务器联系或传输完整响应。 仅在内容更改时,其 URI 保持不变时才使用它,因为绕过重用会增加检索和解码工作。

虚拟化面板中的图像(例如 ListView)

如果图像已从树中删除(因为应用显式删除了它,或者由于它位于新式虚拟化面板中,并且当滚动出视图时被隐式删除),则 XAML 将通过释放映像的硬件资源来优化内存使用量,因为它们不再需要。 内存不会立即释放,而是会在图像元素离开树后的下一秒帧更新期间释放。

因此,应努力使用新式虚拟化面板来托管图像内容列表。

软件光栅化图像

当图像用于非矩形画笔或 NineGrid 时,该图像将使用软件栅格化路径,这将完全不会缩放图像。 此外,它必须在软件和硬件内存中存储映像的副本。 例如,如果将一个图像用作椭圆的画笔,那么较大的完整图像可能会在系统内部被存储两次。 然后,在使用 NineGrid 或非矩形画笔时,你的应用应将其图像预先缩放为呈现时所要使用的相似大小。

后台线程图像加载

XAML 具有内部优化,允许它以异步方式将图像的内容解码到硬件内存中的图面,而无需软件内存中的中间图面。 这减少了峰值内存使用率和呈现延迟。 如果满足以下任一条件,将禁用此功能。

  • 该图像用作 NineGrid
  • CacheMode="BitmapCache" 在图像元素或任何父元素上设置。
  • 图像画笔是非矩形(例如应用于形状或文本时)。

SoftwareBitmapSource

SoftwareBitmapSource 类在不同 WinRT 命名空间(如 BitmapDecoder、相机 API 和 XAML)之间交换可互操作的未压缩图像。 此类可省去 WriteableBitmap 通常需要以及有助于减少峰值内存和源到屏幕延迟的额外副本。

还可以将提供源信息的 SoftwareBitmap 配置为通过自定义 IWICBitmap 提供可重新加载的后备存储,使应用能够根据需要重新映射内存。 这是一个高级 C++ 用例。

应用应使用 SoftwareBitmapSoftwareBitmapSource 与其他生成和使用图像的 WinRT API 进行互操作。 当加载未压缩的图像数据而不是使用 WriteableBitmap 时,应用应使用 SoftwareBitmapSource

将 GetThumbnailAsync 用于缩略图

缩放图像的一个用例是创建缩略图。 尽管你可以使用 DecodePixelWidthDecodePixelHeight 提供小型版本的图像,但 Windows 提供了更高效的 API 来检索缩略图。 GetThumbnailAsync 提供已经在文件系统中缓存的图像的缩略图。 这比 XAML API 提供更好的性能,因为不需要打开或解码图像。

在 Windows 应用 SDK 应用中,使用应用窗口句柄初始化选取器,例如,通过将主窗口存储为 App.MainWindow

FileOpenPicker picker = new FileOpenPicker();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
picker.FileTypeFilter.Add(".bmp");
picker.FileTypeFilter.Add(".jpg");
picker.FileTypeFilter.Add(".jpeg");
picker.FileTypeFilter.Add(".png");
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

StorageFile file = await picker.PickSingleFileAsync();

StorageItemThumbnail fileThumbnail = await file.GetThumbnailAsync(ThumbnailMode.SingleItem, 64);

BitmapImage bmp = new BitmapImage();
await bmp.SetSourceAsync(fileThumbnail);

Image img = new Image();
img.Source = bmp;

解码图像一次

若要防止多次解码图像,请从 Uri 而不是使用内存流分配 Image.Source 属性。 XAML 框架可以将多个位置中的同一 URI 与一个解码的图像相关联,但对于包含相同数据的多个内存流,并且为每个内存流创建不同的解码图像,它无法执行相同的操作。