zoukankan      html  css  js  c++  java
  • 控件UI性能调优 -- SizeChanged不是万能的

    简介

    我们在之前的“UWP控件开发——用NuGet包装自己的控件“一文中曾提到XAML的布局系统 和平时使用上的一些问题(重写Measure/Arrange还是使用SizeChanged?),这篇博文就来为大家简单地描述一下XAML布局系统的行为,并且归纳几个规则。当然真正的XAML布局系统十分复杂,本文无意把情况弄得太复杂,就从一个最简单最直观的例子入手,来为大家提供一点理解XAML布局的新思路。

    问题描述

    假设我们有一个Templated Control,其XAML描述如下:

    <Style TargetType="local:CustomControl1">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:CustomControl1">
                    <Border x:Name="OuterBorder"
                        BorderBrush="Yellow"
                        BorderThickness="20">
                        <Border x:Name="InnerBorder"
                                    BorderThickness="20"
                                    BorderBrush="Red" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    两个Border嵌套,边宽20。我们的目的就是通过代码来改变InnerBorder的大小。比如长宽都变成OuterBorder的一半大。

    首次尝试

    我们很容易就写出了这样的代码:

    public sealed class CustomControl1 : Control
    {
        public CustomControl1() {...}
    
        private Border _border;
        private Border _inner;
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
    
            _border = GetTemplateChild("OuterBorder") as Border;
            _inner = GetTemplateChild("InnerBorder") as Border;
            if (_border != null && _inner != null)
            {
                _border.SizeChanged += (s, e) => {
                    _inner.Width = _border.ActualWidth / 2;
                    _inner.Height = _border.ActualHeight / 2;
                };
            }
        }
    }

    works perfectly。这一实现很好地达到了我们的需求。(而且对于这样的简单的情况设计器还是能够正常处理的)

    对SizeChanged的概述

    但是这却隐藏着问题。首先,SizeChanged事件是由一轮Measure/Arrange完成后触发的。

    XAML的核心布局流程,是从根元素 即页面开始,递归向下。第一次挨个调用Measure,提供能用的大小,并确定每个子项所希望的空间大小;再来一次挨个调用Arrange,提供能用大小,按实际情况给子项分配空间(不一定能满足它们的需要)和确定位置。本例的过程中就涉及到OuterBorderInnerBorder,它们以此能根据Border类布局规则确定自己的大小,即刨去BorderThickness。

    这之后,OuterBorderInnerBorder实际大小就确定了。如果和上次布局的结果不一样,OuterBorder就会触发SizeChanged事件(是Chang*ed*哦),改变InnerBorder设定大小。因为设定大小变化了,会引发新一轮递归Measure和Arrange。这一次之后,OuterBorder的大小不变,InnerBorder的大小变成OuterBorder的一半。之后没有事件和布局再被触发,大家相安无事。

    但实际上,布局进行了两轮。如果Visual Tree很大的话,后果可想而知。

    修改后的过程

    那么,根据我们刚才介绍的过程,从Measure出发,实现如下(去掉SizeChanged的事件绑定并override MeasureOvrride方法):

    public sealed class CustomControl1 : Control
    {
        public CustomControl1() {...}
    
        private Border _border;
        private Border _inner;
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
    
            _border = GetTemplateChild("Border") as Border;
            _inner = GetTemplateChild("InnerBorder") as Border;
        }
    
        protected override Size MeasureOverride(Size availableSize)
        {
            // availableSize就是OuterBorder的大小
            if (_inner != null)
            {
                _inner.Width = availableSize.Width / 2;
                _inner.Height = availableSize.Height / 2;
            }
    
            return base.MeasureOverride(availableSize);
        }
    }

    设定大小后再进入真正的measure环节,一次性搞定布局。原因就在于我们在布局开始之前就搞定了Size信息,而不是在布局结束后再把它辛辛苦苦计算出来的Size踩在地上并让它重来一遍。在我们设定的需求看来,甚至无需插手Arrange流程。

    当然,这免不了地要自己计算Size,可能需要手动减去BorderThickness的大小,甚至还可能要自行调用一次Measure。复杂的具体情况需要具体分析。

    性能对比

    通过调试工具,我们来对比一下两种方法的实际性能:

    SizeChanged MeasureOverride

    在两种实现下,分别大力地快速拖动窗口大小。。。

    其中柱形图是一段时间内UI线程的响应情况,占最大比重的橙色是布局行为。下面的扇形图是选中差不多的时间段内,布局消耗的占比情况。

    可见通过提供Measure策略的方式,即使是这样简单的设定,性能提升也还看得出来。

    如果我们发扬奥卡姆剃刀的精神,不要自己写这陌生的MeasureOverride,用Grid来做如何?

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="2*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Border x:Name="Border"
                BorderBrush="Yellow"
                BorderThickness="20"
                Grid.RowSpan="3" Grid.ColumnSpan="3"/>
        <Border x:Name="InnerBorder"
                BorderThickness="20"
                BorderBrush="Red" 
                Grid.Column="1" Grid.Row="1"/>
    </Grid>

    OnApplyTemplateMeasureOverride都可以不要了,整个code behind十分清爽。行为看起来差不多,那么性能呢?

    想必Grid作为标准控件,优化得应该很好了,但它本身就有一点复杂,和MesureOverride的实现在性能上有一点点差距。但毕竟我们这样简单的例子对于Grid太不公平了,对于更为复杂的情况,还是要使用Grid的。

    总结

    说了这么多,主要是表现一下不必要的布局对于性能的影响,以及对于这样的简单情况如何替代原有实现。

    对于布局有影响的操作大致有:

    • 改变大小:设置WidthHeight、MaxHeight(如果影响到ActualHeight),或者修改MarginThickness
    • 改变内容:设置ContentContentTemplateDataTemplateTextBox.Text
    • 改变某些属性:如VisibleOrientationImage.Stretch
    • 手动调用布局方法:InvalidateMeasureUpdateLayout

    如果调用了这些属性方法,就需要顾虑一下是否会造成不必要的布局了,特别是在SizeChanged这样的由布局触发的事件里。当然这也是一般论,如果控件本来就隐藏了,或者Template改变了原有外型,这些内容也自然随之变化。

    P.S. RenderTransform是不造成重新布局的。

    另外,就本文的例子来说,并不是要大家都把SizeChanged改写成MeasureOverride

    MeasureOverride给了一个好处,就是第一时间获知高层布局的相关信息,也就能赶在布局前最后设置一次属性;SizeChanged能给出复杂布局计算后的最新尺寸,如果自己来计算的话没有意义。总之还是要因地制宜。

    虽然本文的例子十分简单,可能没有多少实际意义,不过希望通过它介绍的流程,能为大家的开发提供一点新的思路。

    参考

    [1] 开源的WPF中的Border.MeasureOverride实现:http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Border.cs,00c166b0e025bc8d

    [2] WPF中的Grid.MeasureOverride实现:http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Grid.cs,f9ce1d6be154348a

    [3] SizeChanged事件参考:https://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.frameworkelement.sizechanged

  • 相关阅读:
    mysql BETWEEN操作符 语法
    mysql IN操作符 语法
    mysql LIKE通配符 语法
    mysql TOP语句 语法
    mysql DELETE语句 语法
    mysql Update语句 语法
    mysql INSERT语句 语法
    mysql ORDER BY语句 语法
    mysql OR运算符 语法
    mysql AND运算符 语法
  • 原文地址:https://www.cnblogs.com/ms-uap/p/5311133.html
Copyright © 2011-2022 走看看