控件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

相關文章
相關標籤/搜索