创建具有平滑动画、高帧速率和高性能媒体捕获和播放的通用 Windows 平台(UWP)应用。
使动画流畅
UWP 应用的关键方面是流畅的交互。 这包括触摸操作,让人感觉“贴合手指”的触感,平滑的过渡和动画,以及用于提供输入反馈的小动作。 在 XAML 框架中,有一个名为合成线程的线程,专用于应用的视觉元素的合成和动画。 由于合成线程独立于 UI 线程(运行框架和开发人员代码的线程),因此应用可以实现一致的帧速率和平滑动画,而不考虑复杂的布局传递或扩展计算。 本部分介绍如何使用合成线程来保持应用的动画流畅。 有关动画的详细信息,请参阅 动画概述。 若要了解如何在执行密集型计算时提高应用的响应能力,请参阅 保持 UI 线程响应。
使用独立动画而不是依赖动画
可以在创建时从头到尾计算独立动画,因为对正在动画处理的属性所做的更改不会影响场景中的其余对象。 因此,独立动画可以在合成线程而不是 UI 线程上运行。 这可以保证它们保持流畅,因为合成线程以一致的节奏进行更新。
所有这些类型的动画都保证是独立的:
使用关键帧的对象动画
零时长动画
Canvas.Left 和 Canvas.Top 属性的动画
UIElement.Opacity 属性的动画
面向 SolidColorBrush.Color 子属性时,将动画应用于 Brush 类型的属性。
针对这些返回值类型的子属性时,以下 UIElement 属性的动画:
依赖动画会影响布局,因此,如果没有 UI 线程的额外输入,则无法计算布局。 依赖动画包括对 宽度 和 高度等属性的修改。 默认情况下,依赖动画不会运行,需要应用开发人员主动选择启用。 启用后,如果 UI 线程保持未阻止状态,则它们会顺利运行,但如果框架或应用在 UI 线程上执行了大量其他工作,它们就会开始停滞不前。
默认情况下,XAML 框架中的所有动画都是独立的,但可以采取一些操作来禁用此优化。 特别要警惕以下情况:
- 将 EnableDependentAnimation 属性设置为允许依赖动画在 UI 线程上运行。 将这些动画转换为独立版本。 例如,对 ScaleTransform.ScaleX 和 ScaleTransform.ScaleY 进行动画处理,而不是对对象的 Width 和 Height 进行动画处理。 不要害怕缩放图像和文本等对象。 该框架仅在 ScaleTransform 进行动画处理时应用双线性缩放。 图像/文本将在最终尺寸处重新栅格化,以确保其始终清晰。
- 对每个帧进行有效的依赖动画更新。 例如,在 CompositionTarget.Rendering 事件的
处理程序中应用转换。 - 在 CacheMode 属性设置为 BitmapCache的元素中运行任何被视为独立的动画。 这被视为依赖性,因为每帧缓存都必须重新进行光栅化。
不要对 WebView 或 MediaPlayerElement 进行动画处理
WebView 控件中的 Web 内容不是由 XAML 框架直接呈现的,它需要额外的工作才能与场景的其余部分组合。 在在屏幕上对控件进行动画处理时,这种额外工作会加起来,并可能会引发同步问题(例如,HTML 内容可能不会与页面上的其余 XAML 内容同步)。 在需要对 WebView 控件进行动画处理时,请在动画持续期间将其与 WebViewBrush 交换。
MediaPlayerElement 动画是一个同样糟糕的想法。 除了性能损失之外,还有可能导致播放视频内容时出现撕裂或其他画面伪影。
注意 本文中 MediaPlayerElement 的建议也适用于 MediaElement。 MediaPlayerElement 仅在 Windows 10 版本 1607 中可用,因此,如果要为早期版本的 Windows 创建应用,则需要使用 MediaElement。
尽量少用无限动画
大多数动画在指定的时间内执行,但将 Timeline.Duration 属性设置为 Forever 允许动画无限期运行。 建议最大程度地减少无限动画的使用,因为它们会持续消耗 CPU 资源,并可能阻止 CPU 进入低功率或空闲状态,从而导致其更快地耗尽电源。
为 CompositionTarget.Rendering 添加处理程序就像在运行无限动画。 通常,UI 线程仅在有工作要做时处于活动状态,但为此事件添加处理程序会强制它运行每个帧。 当没有工作要做时删除处理程序,并在再次需要时重新注册它。
使用动画库
Windows.UI.Xaml.Media.Animation 命名空间包括一个高性能、流畅的动画库,这些动画的外观与其他 Windows 动画保持一致。 相关类的名称中包含“Theme”一词,具体介绍可见于动画概述。 此库支持许多常见的动画方案,例如对应用的第一个视图进行动画处理以及创建状态和内容转换。 我们建议尽可能使用此动画库来提高 UWP UI 的性能和一致性。
注释 动画库无法对所有可能的属性进行动画处理。 有关动画库不适用的 XAML 方案,请参阅 Storyboard 动画。
独立对 CompositeTransform3D 各属性进行动画处理
可以单独为 CompositeTransform3D 的每个属性制作动画,因此只需应用所需的动画即可。 有关示例和详细信息,请参阅 UIElement.Transform3D。 有关动画转换的详细信息,请参阅 情节提要动画 和 关键帧和缓动函数动画。
优化媒体资源
音频、视频和图像是大多数应用使用的令人信服的内容形式。 随着媒体捕获速率的提高和内容从标准定义移动到高清,存储、解码和播放此内容所需的资源量将增加。 XAML 框架基于添加到 UWP 媒体引擎的最新功能构建,以便应用免费获得这些改进。 下面我们将介绍一些其他技巧,这些技巧允许你在 UWP 应用中充分利用媒体。
释放媒体流
应用程序通常使用的资源中,媒体文件是最常见且昂贵的。 由于媒体文件资源可以大大增加应用内存占用量的大小,因此在应用使用完后,必须记住立即释放媒体的句柄。
例如,如果你的应用使用 RandomAccessStream 或 IInputStream 对象,请确保在应用使用完对象后对对象调用 close 方法,以释放基础对象。
尽可能显示全屏视频播放
在 UWP 应用中,始终使用 MediaPlayerElement 上的 IsFullWindow 属性来启用和禁用全窗口呈现。 这可确保在媒体播放期间使用系统级别优化。
当 XAML 框架只渲染视频内容时,可以优化视频的显示效果,从而在使用更少电量的情况下提供更高的帧率体验。 对于最有效的媒体播放,请将 MediaPlayerElement 的大小设置为屏幕的宽度和高度,并且不显示其他 XAML 元素
有理由在 MediaPlayerElement 上覆盖 XAML 元素,这些元素占据屏幕的完整宽度和高度,例如隐藏式字幕或瞬间传输控件。 确保隐藏这些元素(设置 Visibility="Collapsed"
),当不需要它们时,使媒体播放恢复到其最有效状态。
显示停用和节省电源
若要在不再检测到用户操作(例如应用正在播放视频时)后防止系统停用显示,可以调用 displayRequest.RequestActive。
若要节省电源和电池使用时间,应调用 DisplayRequest.RequestRelease,以在不再需要显示请求后立即释放显示请求。
以下是您应该释放显示请求的情况:
- 视频播放被暂停,例如因用户操作、缓冲或由于带宽有限而调整。
- 播放停止。 例如,视频播放结束或演示文稿结束。
- 出现播放错误。 例如,网络连接问题或损坏的文件。
将其他元素置于嵌入视频的一侧
通常,应用提供嵌入视图,其中视频在页面中播放。 现在,很明显您失去了全屏优化,因为 MediaPlayerElement 的尺寸不符合页面,并且还绘制了其他 XAML 对象。 请注意,通过绘制 MediaPlayerElement周围的边框,无意中进入此模式。
不要在视频处于嵌入模式时在其顶部绘制 XAML 元素。 如果这样做,框架将被迫进行一些额外的处理来构建场景。 将传输控件放置在嵌入式媒体元素下面,而不是放在视频顶部是针对这种情况进行优化的好示例。 在此图像中,红色条指示一组传输控件(播放、暂停、停止等)。
不要将这些控件放在未全屏的媒体上。 而是将传输控件放置在呈现媒体的区域之外的某个位置。 在下一个图像中,控件放置在媒体下方。
MediaPlayerElement 与相邻的元素 配合使用
延迟设置 MediaPlayerElement 的源
媒体引擎成本高昂,XAML 框架会尽可能延迟加载 DLL 和创建大型对象。 MediaPlayerElement 在通过 Source 属性设置其源后被迫进行此操作。 当用户真正准备好播放媒体时,设定此项设置能够将与 MediaPlayerElement 相关的大部分成本尽可能拖延到更后。
设置 MediaPlayerElement.PosterSource
设置 MediaPlayerElement.PosterSource 使 XAML 能够释放本来会使用的某些 GPU 资源。 此 API 允许应用尽可能少地使用内存。
改进媒体清理
清理始终是媒体平台做出真正响应的艰巨任务。 通常,人们可以通过更改滑块的值来实现此目的。 下面是有关如何使此操作尽可能高效的几个提示:
- 使用计时器查询 MediaPlayerElement.MediaPlayer上的 位置,以更新 Slider 的值。 请确保对计时器使用合理的更新频率。 Position 属性仅在播放过程中每 250 毫秒更新一次。
- 滑块上步骤频率的大小必须与视频的长度一起缩放。
- 订阅滑块上的 PointerPressed、PointerMoved和 PointerReleased 事件,用来将用户拖动滑块拇指时的 PlaybackRate 属性设置为 0。
- 在 PointerReleased 事件处理程序中,手动将媒体位置设置为滑块位置值,以实现拖动时的最佳捕捉效果。
将视频分辨率与设备分辨率匹配
解码视频需要大量的内存和 GPU 周期,因此请选择靠近其分辨率的视频格式。 如果 1080 视频要缩小到更小的尺寸,那么使用资源解码就没有意义。 许多应用没有以不同分辨率编码的相同视频;但如果可用,请使用接近其显示分辨率的编码。
选择建议的格式
媒体格式选择可以是敏感主题,通常由业务决策驱动。 从 UWP 性能的角度来看,我们建议将 H.264 视频作为主要视频格式,AAC 和 MP3 作为首选音频格式。 对于本地文件播放,MP4 是视频内容的首选文件容器。 H.264 解码通过最新的图形硬件加速。 此外,尽管 VC-1 解码的硬件加速已广泛提供,但对于市场上的大量图形硬件,在许多情况下,加速仅限于部分加速级别(或 IDCT 级别),而不是全蒸汽级硬件卸载(即 VLD 模式)。
如果完全控制了视频内容生成过程,则必须了解如何在压缩效率和 GOP 结构之间保持良好的平衡。 相对较小的 GOP 大小与 B 图片可以增加查找或技巧模式的性能。
如果包括短、低延迟的音频效果(例如在游戏中),请使用带有未压缩 PCM 数据的 WAV 文件,以减少压缩音频格式的典型处理开销。
优化图像资源
将图像缩放为适当的大小
图像以非常高的分辨率捕获,这可能会导致应用在从磁盘加载映像数据时使用更多的 CPU 和更多内存。 但是,没有必要在内存中解码和保存高分辨率图像,只是为了将其显示得比原始大小更小。 相反,使用 DecodePixelWidth 和 DecodePixelHeight 属性创建在它将被显示的确切大小的图像版本。
别这样:
<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>
DecodePixelWidth 和 DecodePixelHeight 的单位默认为物理像素。 DecodePixelType 属性可用于更改这一行为:将 DecodePixelType 设置为 逻辑 后,解码大小会自动根据系统当前的比例因子进行调整,这与其他 XAML 内容的处理方式类似。 例如,如果希望 DecodePixelWidth 和 DecodePixelHeight 匹配图像控件的高度和宽度属性,那么通常应将 DecodePixelType 设置为 逻辑。 当使用物理像素的默认行为时,你必须亲自考虑系统的当前缩放比例;此外,如果用户更改显示首选项,则应监听缩放更改的通知。
如果 DecodePixelWidth/Height 显式设置为比图像在屏幕上显示的尺寸还大,应用程序将不必要地消耗额外内存—每个像素最多4个字节—这对于大型图像来说会迅速变得昂贵。 图像还将使用双线性缩放进行缩减,这可能导致图像在缩放比例较大时显得模糊。
如果显式设置的 DecodePixelWidth/DecodePixelHeight 小于图像在屏幕上的显示尺寸,那么图像会被放大,并可能出现像素化。
在某些情况下,如果无法提前确定适当的解码大小,您应该依赖 XAML 的自动合适大小解码功能。如果未指定显式的 DecodePixelWidth/DecodePixelHeight,XAML 会尽量尝试以适当的大小解码图像。
如果提前知道图像内容的大小,则应设置显式解码大小。 如果提供的解码大小是相对于其他 XAML 元素大小的,那么您还应将 DecodePixelType 设置为 Logical。 例如,如果使用 Image.Width 和 Image.Height 显式设置内容大小,则可以将 DecodePixelType 设置为 DecodePixelType.Logical 以使用与图像控件相同的逻辑像素维度,然后显式使用 BitmapImage.DecodePixelWidth 和/或 BitmapImage.DecodePixelHeight 来控制图像的大小,以实现潜在的大内存节省。
请注意,在确定解码内容大小时,需要考虑 Image.Stretch。
大小正确的解码
如果未设置显式解码大小,XAML 会尽力通过将图像解码为屏幕中显示的确切大小(根据包含页面的初始布局)来尽力保存内存。 建议尽可能使用此功能的方式编写应用程序。 如果满足以下任一条件,将禁用此功能。
- 在通过 SetSourceAsync 或 UriSource设置内容后,BitmapImage 将连接到实时 XAML 树。
- 使用同步解码(如 SetSource)解码图像。
- 通过在主机图像元素、画笔或任何父元素上将 不透明度 设置为 0,或将 可见性 设置为 折叠,来隐藏图像。
- 图像控件或画笔使用 拉伸,使用 无。
- 该图像用作 九宫格。
-
CacheMode="BitmapCache"
被设置在图像元素或任何父元素上。 - 图像画笔是非矩形的,例如,当应用于形状或文本时。
在上述方案中,设置显式解码大小是实现内存节省的唯一方法。
设置源之前,应始终将 BitmapImage 附加到实时树中。 每当在标记中指定图像元素或画笔时,这将自动成为这种情况。 下面提供了“实时树示例”标题下的示例。 应始终避免使用 SetSource,并在设置流源时改用 SetSourceAsync。 在等待 ImageOpened 事件引发时,最好避免通过将不透明度设为零或折叠可见性来隐藏图像内容。 这样做是一种主观判断:如果这样做,你将无法受益于自动调整到合适大小的解码过程。 如果应用最初必须隐藏图像内容,则它还应尽可能显式设置解码大小。
实时树示例
示例 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;
缓存优化
使用 UriSource 从应用程序包或网络加载内容的图像适用缓存优化。 URI 用于唯一标识基础内容,在内部 XAML 框架不会多次下载或解码内容。 相反,它将使用缓存的软件或硬件资源多次显示内容。
此优化例外是,如果图像以不同的分辨率多次显示(可以显式指定或通过自动正确大小的解码指定)。 每个缓存条目还存储图像的分辨率,如果 XAML 找不到与所需分辨率匹配的源 URI 的图像,则它将以该大小解码新版本。 但是,它不会再次下载编码的图像数据。
因此,在从应用包加载图像时,应使用 UriSource,避免在不需要时使用文件流和 SetSourceAsync。
虚拟化面板中的图像(例如 ListView)
如果图像已从树中删除(因为应用程序显式地删除了它,或者由于它位于现代虚拟化面板中,当滚动出视图时被隐式删除),XAML 将通过释放图像的硬件资源来优化内存使用,因为这些资源已不再需要。 内存不会立即释放,而是在图像元素不再位于树中一秒后,在帧更新期间释放。
因此,应努力使用新式虚拟化面板来托管图像内容列表。
软件光栅化图像
当图像被用于非矩形画笔或 NineGrid时,将会采用软件光栅化路径,这样做完全不会缩放图像。 此外,它必须在软件和硬件内存中存储映像的副本。 例如,如果将图像用作椭圆的画笔,则在内部存储可能较大的完整图像两次。 使用 NineGrid 或非矩形画笔时,你的应用应将其图像预先缩放到大约最终显示的大小。
后台线程图像加载
XAML 具有内部优化,允许它以异步方式将图像的内容解码到硬件内存中的图面,而无需软件内存中的中间图面。 这减少了峰值内存使用率和呈现延迟。 如果满足以下任一条件,将禁用此功能。
- 该图像用作 九宫格。
-
CacheMode="BitmapCache"
被设置在图像元素或任何父元素上。 - 图像画笔是非矩形的,例如,当应用于形状或文本时。
SoftwareBitmapSource
SoftwareBitmapSource 类在不同的 WinRT 命名空间(如 BitmapDecoder、相机 API 和 XAML)之间交换可互操作的未压缩图像。 此类可以避免通常在 WriteableBitmap中所需的额外副本,有助于降低峰值内存使用并减少从源到屏幕的延迟。
还可以将提供源信息的 SoftwareBitmap 配置为使用自定义 IWICBitmap 来提供可重新加载的后备存储,使应用能够根据需要重新映射内存。 这是一个高级C++用例。
应用应使用 SoftwareBitmap 和 SoftwareBitmapSource 与其他生成和使用图像的 WinRT API 进行互操作。 应用在加载未压缩的图像数据时,应使用 SoftwareBitmapSource,而不是使用 WriteableBitmap。
使用 GetThumbnailAsync 以获取缩略图
缩放图像的一个用例是创建缩略图。 尽管可以使用 DecodePixelWidth 和 DecodePixelHeight 来提供较小的图像版本,但 UWP 提供了更高效的 API 来检索缩略图。 GetThumbnailAsync 为图像提供缩略图,这些图像的文件系统已被缓存。 这比 XAML API 提供更好的性能,因为不需要打开或解码图像。
FileOpenPicker picker = new FileOpenPicker();
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();
bmp.SetSource(fileThumbnail);
Image img = new Image();
img.Source = bmp;
Dim picker As New FileOpenPicker()
picker.FileTypeFilter.Add(".bmp")
picker.FileTypeFilter.Add(".jpg")
picker.FileTypeFilter.Add(".jpeg")
picker.FileTypeFilter.Add(".png")
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary
Dim file As StorageFile = Await picker.PickSingleFileAsync()
Dim fileThumbnail As StorageItemThumbnail = Await file.GetThumbnailAsync(ThumbnailMode.SingleItem, 64)
Dim bmp As New BitmapImage()
bmp.SetSource(fileThumbnail)
Dim img As New Image()
img.Source = bmp
解码图像一次
若要防止多次解码图像,请从 Uri 中分配 Image.Source 属性,而不是使用内存流。 XAML 框架可以将多个位置中的同一 URI 与一个解码的图像相关联,但对于包含相同数据的多个内存流,并且为每个内存流创建不同的解码图像,它无法执行相同的操作。