WPF性能优化之延迟加载(解决页面卡顿问题)

文章目录

  • 前言
  • [一. 基础知识回顾](#一. 基础知识回顾)
  • [二. 问题分析](#二. 问题分析)
  • [三. 解决方案](#三. 解决方案)
    • [1. 新建一个名为DeferredContentHost的控件。](#1. 新建一个名为DeferredContentHost的控件。)
    • [2. 在DeferredContentHost控件中定义一个名为Content的object类型的依赖属性,用于承载要加载的子控件。](#2. 在DeferredContentHost控件中定义一个名为Content的object类型的依赖属性,用于承载要加载的子控件。)
    • [3. 在DeferredContentHost控件中定义一个名为Skeleton的object类型的依赖属性,用于在子控件加载前显示骨架屏效果(使用加载效果也可以)。](#3. 在DeferredContentHost控件中定义一个名为Skeleton的object类型的依赖属性,用于在子控件加载前显示骨架屏效果(使用加载效果也可以)。)
    • [4. 在DeferredContentHost控件Loaded时显示骨架屏。](#4. 在DeferredContentHost控件Loaded时显示骨架屏。)
    • [5. 在DeferredContentHost控件显示骨架屏后执行Dispatcher.BeginInvoke(),将子控件显示的代码添加到Dispatcher消息队列。](#5. 在DeferredContentHost控件显示骨架屏后执行Dispatcher.BeginInvoke(),将子控件显示的代码添加到Dispatcher消息队列。)
  • [四. 运行效果](#四. 运行效果)
    • [4.1 未优化的效果](#4.1 未优化的效果)
    • [4.2 优化后的效果](#4.2 优化后的效果)

前言

长久以来,WPF的性能一直为人所诟病,其中很大一个原因就是因为WPF在开发过程中,稍有不慎就会阻塞UI线程,导致操作卡顿,甚至页面停止响应。它的原因当然是多方面的,我们今天只讨论比较常见的情况,并给出解决方案,让您开发的软件尽量减少卡顿。


一. 基础知识回顾

我们都知道WPF是单线程模型,所有UI元素必须由创建它们的线程直接操作,并且该线程还负责处理用户输入(鼠标、键盘)、渲染界面、执行事件处理程序、管理布局和动画等工作。所以如果UI线程一旦被阻塞,就会导致灾难性后果,反应到界面上就是卡顿,鼠标无法操作,也不响应键盘输入。

二. 问题分析

当加载一个Window时会执行一系列操作,其中最重要的操作就是布局的测量(Measure )与排列(Arrange),布局系统会从Window的根元素开始沿可视化树逐个调用子级控件的Measure与Arrange方法,以确认页面上的每个控件被渲染到正确的位置。在理想的情况下这种模式可以工作得很好,但是在实际项目中我们往往会在页面上嵌套数量庞大的子控件来实现功能,当要渲染的控件数量达到UI线程处理的瓶颈上限或是在布局计算中耗时过多,这时就可能会导致UI线程被阻塞。

三. 解决方案

既然UI线程不能无限制处理所有请求,那我们给它排个队,一个一个处理不就可以解决这个问题了。在WPF中所有控件都继承自DispatcherObject类,DispatcherObject类中有一个名为Dispatcher的属性,Dispatcher就是管理UI线程消息队列的核心。我们只需要将控件的加载任务送入Dispatcher,让它在合适的时机执行就可以了。以下是实现的过程:

1. 新建一个名为DeferredContentHost的控件。

2. 在DeferredContentHost控件中定义一个名为Content的object类型的依赖属性,用于承载要加载的子控件。

3. 在DeferredContentHost控件中定义一个名为Skeleton的object类型的依赖属性,用于在子控件加载前显示骨架屏效果(使用加载效果也可以)。

4. 在DeferredContentHost控件Loaded时显示骨架屏。

5. 在DeferredContentHost控件显示骨架屏后执行Dispatcher.BeginInvoke(),将子控件显示的代码添加到Dispatcher消息队列。

以上代码的核心在于Dispatcher.BeginInvoke(DispatcherPriority priority, Delegate method)方法中的priority参数,该参数用于指定method委托在消息队列中的执行优先级,以下为DispatcherPriority枚举的所有值:

从上图可以看出,为了不影响数据绑定、界面渲染、用户输入等操作,我们应该选择尽量低的优先级来执行子控件显示的代码。这里我们使用ContextIdle。以下是完整的代码:

csharp 复制代码
[ContentProperty("Content")]
public class DeferredContentHost : FrameworkElement
{
    #region Fields
    private ContentControl _container = new ContentControl();
    #endregion
    #region Methods
    public DeferredContentHost()
    {
        this.AddVisualChild(_container);
        this.Loaded += DeferredContentHost_Loaded;
    }
    private void DeferredContentHost_Loaded(object sender, RoutedEventArgs e)
    {
        if (IsInDesignMode)
        {
            this._container.Content = this.Content;
        }
        else
        {
            this._container.Content = this.Skeleton;
            this.Dispatcher.BeginInvoke((Action)(() => this._container.Content = this.Content), System.Windows.Threading.DispatcherPriority.ContextIdle);
        }
    }
    protected override Visual GetVisualChild(int index)
    {
        return _container;
    }
    protected override Size MeasureOverride(Size availableSize)
    {
        _container.Measure(availableSize);
        if (availableSize.Width == double.PositiveInfinity || availableSize.Height == double.PositiveInfinity)
        {
            return _container.DesiredSize;
        }
        return availableSize;
    }
    protected override Size ArrangeOverride(Size finalSize)
    {
        _container.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
        return base.ArrangeOverride(finalSize);
    }
    #endregion
    #region Properties
    protected override int VisualChildrenCount => 1;
    protected bool IsInDesignMode { get => DesignerProperties.GetIsInDesignMode(this); }
    public object Content
    {
        get { return (object)GetValue(ContentProperty); }
        set { SetValue(ContentProperty, value); }
    }
    // Using a DependencyProperty as the backing store for UIElement.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ContentProperty =
        DependencyProperty.Register("Content", typeof(object), typeof(DeferredContentHost));
    public object Skeleton
    {
        get { return (object)GetValue(SkeletonProperty); }
        set { SetValue(SkeletonProperty, value); }
    }
    // Using a DependencyProperty as the backing store for Skeleton.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SkeletonProperty =
        DependencyProperty.Register("Skeleton", typeof(object), typeof(DeferredContentHost));
    #endregion
}

四. 运行效果

我们用大图片来模拟阻塞UI线程的情况,下面是两种效果对比。

4.1 未优化的效果

xml 复制代码
<DataTemplate x:Key="item1">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Image Margin="5" Source="{Binding FullName}" />
        <TextBlock
            Grid.Row="1"
            Margin="5,0,5,5"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Text="{Binding Name}"
            TextTrimming="WordEllipsis" />
    </Grid>
</DataTemplate>

一次加载所有大图片,界面停止响应,文本框无法输入文字。

4.2 优化后的效果

xml 复制代码
<DataTemplate x:Key="item2">
    <controls:DeferredContentHost>
        <controls:DeferredContentHost.Skeleton>
            <controls:Skeleton>
                <controls:SkeletonGroup Orientation="Vertical">
                    <controls:SkeletonItem
                        Height="*"
                        Margin="5"
                        RadiusX="5"
                        RadiusY="5" />
                    <controls:SkeletonItem
                        Width="120"
                        Height="30"
                        Margin="5,0,5,5"
                        HorizontalAlignment="Center"
                        RadiusX="5"
                        RadiusY="5" />
                </controls:SkeletonGroup>
            </controls:Skeleton>
        </controls:DeferredContentHost.Skeleton>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>
            <Image Margin="5" Source="{Binding FullName}" />
            <TextBlock
                Grid.Row="1"
                Margin="5,0,5,5"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Text="{Binding Name}"
                TextTrimming="WordEllipsis" />
        </Grid>
    </controls:DeferredContentHost>
</DataTemplate>

使用DeferredContentHost控件的延迟加载效果,加载过程文本框可以输入文字,界面可以正常响应鼠标操作。


技术交流

QQ群:661224882

相关推荐
jump_jump3 天前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化
小小工匠4 天前
Redis - 事务机制:能实现 ACID 属性吗
数据结构·redis·性能优化·并发·持久化
大鱼>4 天前
地平线BPU部署实战:YOLOv8在J5/X3上的算法适配与性能优化
算法·yolo·性能优化
醉颜凉4 天前
Elasticsearch高性能优化:Bulk API大规模数据导入性能调优全攻略
elasticsearch·性能优化·jenkins
隔窗听雨眠4 天前
C语言函数递归从入门到精通(下):性能优化与工程实践
c语言·算法·性能优化
Chris _data4 天前
WPF 学习第三天 — Modbus RTU 串口通信
hadoop·学习·wpf
昇腾CANN4 天前
【cann-samples系列】GroupedMatmul MX量化矩阵乘的深度性能优化实践
线性代数·性能优化·矩阵·昇腾·cann
霸道流氓气质4 天前
Spring Boot 微服务性能优化完全指南
spring boot·微服务·性能优化
步步为营DotNet4 天前
Blazor 与 Microsoft.Extensions.AI 在客户端性能优化中的协同应用
人工智能·microsoft·性能优化
不能只会打代码5 天前
边缘视频分析平台的架构设计与性能优化——从750ms到190ms的调优之路
java·spring boot·redis·性能优化·边缘计算·物联网竞赛