Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

01 Dec 2020

UWP开发档【XAML性能】阅读笔记

Develop > Debugging, testing, and performance > Performance and XAML UI 文档笔记。

ListView and GridView UI optimization

使用UI虚拟化、元素缩减以及渐进式更新来改善LitView和GridView的性能。

UI虚拟化的意思就是不用一次为所有的数据创建UI元素。对于ListView来说,支持虚拟化的UI面板包括ItemsWrapGrid以及ItemsStackPanel。其他的,比如VariableSizedWrapGrid, WrapGrid, 或者StackPanel不支持虚拟化。

ListView的这些事件ChoosingGroupHeaderContainer, ChoosingItemContainer,以及ContainerContentChanging只会为ItemsWrapGride或者ItemsStackPanel触发。

为了确定哪些UI需要绘制,哪些不需要绘制,要首先确定UI的视界(ViewPort),即可以用来绘制子元素的预取。注意有一些容器,比如ScrollViewer还有Grid,不会限制子元素的大小,所以子元素的大小并不取决于视界的大小。把ItemsControl放置在这样的容器中,虚拟化不会默认起作用,除非为ItemsControl设置一个明确的大小。

优化性能的另一个思路是减少需要显示的元素,可以参考 Optimize your XAML markup获取一些额外的信息。

ListViewItem和GridViewItem默认都包含一个为UI显示优化过的ListViewItemPresenter元素,可以支持聚焦、选中,以及其他UI状态。如果你自定义了容器样式(ItemContainerStyle))或者自定义了容器模板,那么推荐在自定义中使用ListViewItemPresenter,可以通过ListViewItemPresenter的辖属来对其进行自定义。下面是一个例子:

...
<ListView>
 ...
 <ListView.ItemContainerStyle>
 <Style TargetType="ListViewItem">
 <Setter Property="Template">
 <Setter.Value>
 <ControlTemplate TargetType="ListViewItem">
 <ListViewItemPresenter SelectionCheckMarkVisualEnabled="False" SelectedBackground="Orange"/>
 </ControlTemplate>
 </Setter.Value>
 </Setter>
 </Style>
 </ListView.ItemContainerStyle>
</ListView>
<!-- ... -->

如果ListViewItemPresenter的25个辖属还不够你用,那么只好自定义ListViewItemExpanded和GridViewItemExpanded这两个控件模板(来自于\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<version>\Generic\generic.xaml)。

分段加载可以在用户快速滚动的时候保持UI的响应性。除非在ShowsScrollingPlaceholders设置了false,默认情况下,会显示占位符。可以指定一个元素的x:Phase以及{x:Bind},如下所示:

                       <TextBlock Text="{x:Bind Title}"/>
                        <TextBlock Text="{x:Bind Subtitle}" x:Phase="1"/>
                        <TextBlock Text="{x:Bind Description}" x:Phase="2"/>

快速滚动的时候,会先绘制由ShowsScrollingPlaceholders控制的占位符,然后是Title,接着是SubTitle,最后是description。

渐进式的data template更新是在 ContainerContentChanging里面设置不需要元素的Opacity来将其隐藏。当元素被回收的时候会保留旧有值。通过事件参数的Phase属性来决定哪些元素需要显示或者隐藏。如果有额外的阶段,则需要注册一个回调。这些回调就是不同的Phase了。

如果不同的条目对应不同的显示界面(即数据模板)的情况下,可以使用ChoosingItemContainer事件应对。ChoosingItemContainer可以返回所需的LIstViewItem或者GridViewItem供列表使用。ChoosingGroupHeaderContainer提供相似的功能,不过针对的是群组标题。另一个办法是使用DataTemplateSelector,不过没有ChoosingItemContainer高效。

ListView and GridView data virtualization

虚拟化有两种,一种是惰性加载,另一种是随机加载。

要实现惰性加载,数据源必须实现下面的接口

  • IList
  • INotifyCollectionChanged (C#) 或 IObservableVector<T> (C++)
  • ISupportIncrementalLoading

一个数据源是一个驻留内存的列表。ItemsControl会通过标准的IList接口来像数据源要数据,以及数据的个数。这里的数据个数表示的是驻留列表的大小而不是数据集的大小。当控件快划到头的时候,会调用ISupportIncrementalLoading.HasMoreItems来问是否有更多数据。如果有的话可以通过ISupportIncrementalLoading.LoadMoreItemsAsync来返回更多数据。你可以加载比所要数据更多的数据。然后通过INotifyCollectionChanged or IObservableVector<T> 来通知数据加载的完成。

一个例子是Windows8的XAML data binding sample

要实现随机加载,则要实现以下接口

  • IList
  • INotifyCollectionChanged (C#/VB) or IObservableVector<T> (C++/CX)
  • (Optionally) IItemsRangeInfo
  • (Optionally) ISelectionInfo

IItemsRangeInfo给出以下信息:

  • 在视界中显示的信息
  • ItemsControl已拥有的数据条目,还有当前条目,以及第一个条目

你的数据源可以根据IItemsRangeInfo给出的信息加载或者卸载某些条目。单个IItemsRangeInfo只能给单个ItemsControl使用。

一些基本的策略

  • 当数据源被要求返回条目的时候
    • 如果条目在内存中,则直接返回条目
    • 如果没有,则返回null或者占位符
    • 去数据集获取条目,获取完通过通知告诉ItemsControl
  • (可选),但视界变化时,决定哪些条目需要留在内存

其他注意事项:

  • 使用异步操作来获取数据,不要阻断UI
  • 获取数据的时候不要计算太精细,prefer chunky to chatty。
  • 决定多少请求可以同时进行,一次一个可能会降低响应度
  • 可以取消数据获取吗?
  • 考虑每次获取数据的代价。
  • 数据会从远端改变吗?比如说在33的位置插入了一个元素。
  • 是否支持预先获取,比如从滚动的方向和速率上做一个预判
  • 什么时候从不要的条目从内存中删除呢?

其他参考

参考:Dramatically Increase Performance when Users Interact with Large Amounts of Data in GridView and ListView