UWP下控件开发04 - 虚拟化面板实现

UWP 应用不仅仅是在 PC 上,更有可能在平板,或者 Windows 10 Mobile 上运行,在硬件没有 PC 那么强大的条件下,我们的面板在大量数据的情况下还能正常使用吗?那么今天,我们就开始对面板的布局进行优化,让面板在大量的数据和大量的子元素存在的同时,减少硬件消耗,不严重影响性能.

(注意:本节内容可能有错误或者不确定元素,请带着怀疑的态度来看)

首先最容易想到的就是虚拟化,虚拟化分为两种,UI虚拟化与数据虚拟化.

UI虚拟化的意思就是在一个元素不在可视范围中时,释放该元素,之后需要的时候实例化.

例如:

在 ScrollView 中,系统会一次性渲染所有的 ScrollView 包含的元素,而 ListView 就包含了一个 ScrollView ,由于系统默认在 ListView 中用的 ItemsPanelVirtualizingStackPanel ,这个面板是支持虚拟化的,使用了这个面板之后,系统就不会去渲染处在屏幕外的元素,这就能极大的节省内存消耗与性能消耗,当 ScrollView 中的元素变多之后,滑动性能与载入性能都会下降.

数据虚拟化则主要出现在大量数据(上万条数据)的情况下,就算这些数据并没有图像等占内存的东西,数据大起来之后,占的内存也很可观,这个时候就需要进行数据虚拟化,将数据放入储存空间,每次只取需要的那一部分,让载入内存的数据数量减下来.

这次主要是想讨论以下 UWP 框架下如何实现 UI 虚拟化,之前我们说过了在 SL 或者 WPF 中是可以继承 System.Windows.Controls.VirtualizingPanel 来自己实现虚拟化面板布局的.但是在 Runtime 和 UWA 中似乎虚拟化涉及到了更底层的东西,所以并不向外开放.

之后我实验了用 Windows.UI.Xaml.Controls.ItemContainerGenerator 类来虚拟化 Item ,但是并不能成功,这个类在 WPF 中是可以对子元素进行虚拟化与实例化的,此路不通,之后我有想了很多虚拟化的方案,例如:

01.用一个 ControlContent 属性储存 Content ,虚拟化的时候将 Content 设置为 Null ,从可视化树中移除,需要的时候从 ControlContent 中获取,赋值给 Content ,让可视化树呈现内容.

02.直接虚拟化时将 Content 设置为不可见

但是这些方案都失败了,不但不能减少内存还把原来的 120MB 的消耗提升到了 600MB ,在内容呈现后,保留该内容的呈现会出现内存暴涨的情况,原因不明.

最后,我弄出来了两种虚拟化方案:

01.是上面01方案的升级版本,不在保留原内容的 Content ,而是通过保留 ViewModel 来与生成器配合实时生成 View.

02.将虚拟化的过程交给开发者,根据不同的内容来选择不同的虚拟化,例如,大量的图片的项目,只需要将 Image.Source 的引用清空就能回收内存,实例化只需要生成一个新的 ImageSource 给 Image 就可以了.

这两种方案在 HLib 中的体现就是 VirtualizingItem 与 CustomVirtualizingItem 两个支持虚拟化的子元素,使用 VirtualizingItem 需要提供一个实现 IItemGenerator 接口的 ViewModel ,他能自生成 View 提供给可视化树呈现内容. CustomVirtualizingItem 则需要用户实现一个 IVirtualizingItem 接口的 View ,将这个 View 赋值给 CustomVirtualizingItem.Content 会自动调用接口来实现虚拟化.

下面来看以下双方的代码:

VirtualizingItem :

/// <summary>
/// 可重写,实现虚拟化子项的过程
/// </summary>
protected virtual void OnVirtualize()  
{
    if(!IsVirtualized)
    {
        IsInternalChangeContent = true;
        this.IsVirtualized = true;
        RealSize = this.DesiredSize;
        //从可视化树移除内容
        this.Content = null;
    }
}
 
/// <summary>
/// 可重新,实现实例化子项的过程
/// </summary>
protected virtual void OnRealize()  
{
    if(IsVirtualized)
    {
        IsInternalChangeContent = true;
        this.IsVirtualized = false;
        //从可视化树呈现内容
        this.Content = this.ControlContent.GenerateItem();
    }
}

CustomVirtualizingItem:

/// <summary>
/// 可重写,实现虚拟化子项的过程
/// </summary>
protected virtual void OnVirtualize()  
{
    if (!IsVirtualized)
    {
        this.IsVirtualized = true;
        RealSize = this.DesiredSize;
        //要求 Content 实现接口并进行虚拟化
        if(this.Content is Primitives.IVirtualizingItem)
        {
            (this.Content as Primitives.IVirtualizingItem).Virtualize();
        }
    }
}
 
/// <summary>
/// 可重新,实现实例化子项的过程
/// </summary>
protected virtual void OnRealize()  
{
    if (IsVirtualized)
    {
        this.IsVirtualized = false;
        //要求 Content 实现接口并进行实例化
        if (this.Content is Primitives.IVirtualizingItem)
        {
            (this.Content as Primitives.IVirtualizingItem).Realize();
        }
    }
}

具体的代码可以去我的 GitHub 上面查看,大概的实现就是这样了. 使用 VirtualizingItem 则只需要一个实现了 IItemGenerator 接口的 ViewModel 通过 GenerateItem() 方法返回 View ,之后添加进 ListView 只需要新建一个 VirtualizingItem 将 ViewModel 的值赋值给 VirtualizingItem.ControlContent 就可以了.具体使用可以看 HLib.Test 的实例.

虚拟化的过程解决了之后只需要在 VirtualizingPanel 中监听 ScrollView.ViewChanging 事件,计算需要虚拟化的子元素,进行虚拟化与实例化就可以了.这样的一个 VirtualizingPanel 已经可以完成虚拟化工作,经过我的测试, 120MB 的内存占用降至了 50MB 左右,在 PC 上,由 240MB 降至了 140MB,效果显著.

非虚拟化实现 (252MB)

J7NfAvq

虚拟化实现 (139MB)

BfiqIbN

目前就是计算需要虚拟化的子元素的过程是整个枚举,效率底下,目前在寻找更优的解决方案.