控件UI性能调优 -- SizeChanged不是万能的

简介

咱们在以前的“UWP控件开发——用NuGet包装本身的控件“一文中曾提到XAML的布局系统 和平时使用上的一些问题(重写Measure/Arrange仍是使用SizeChanged?),这篇博文就来为你们简单地描述一下XAML布局系统的行为,而且概括几个规则。固然真正的XAML布局系统十分复杂,本文无心把状况弄得太复杂,就从一个最简单最直观的例子入手,来为你们提供一点理解XAML布局的新思路。html

 

问题描述

假设咱们有一个Templated Control,其XAML描述以下:windows

<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的一半大。app

 

首次尝试

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

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很大的话,后果可想而知。ui

 

修改后的过程

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

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

相关文章
相关标签/搜索