Windows 10 UWP 31 of N: Build 2017 What`s new performance UI tips and trick in UWP

  • 170
  • 0
  • UAP
  • 2021-04-30

介紹在Creator update針對UWP的效能有那些小技巧

今天要介紹的是MSFT在2017年3月推出的Creator update(創作者更新)的UWP架構APP的UI效能調整的要點~

先從UI(XAML)的變更開始說起,重一開始(10240)的UWP就支援所謂的"延遲載入XAML"的特性也就是 DeferLoadStrategy 的XAML語法。

先說明一下 DeferralLoadStrategy 需要幾點注意特性

  1. 只能放在具有Name的Control上。
  2. 能使用的Control是UIElement和FlyoutBase。
  3. 不能放在Page、UserControl、DataTemplate的根上(也就是Page、UserControl、DataTemplate本身不能使用DeferLoadStrategy當以上元素是VisualTree。
  4. 在ResourceDictionary的元件不能使用。
  5. 在XamlReader.Load無法使用。
  6. 如果Parent的Element被移除會導致還沒有被Load的Element被移除掉。

所以要如何找到 Load Element(載入)呢?有以下幾點方式

  1. 使用FindName的方法。
  2. 使用GetTemplateChild的方法。
  3. 在VisualState裡面使用Setter和Storyboard可以找到
  4. 在任一Storyboard上參考到。
  5. 使用Binding資料繫結指定目標在Defer的Element上。

在Xaml的語法會變成如下所示

<TextBox x:Name="LazyTbx" x:DeferLoadStrategy="Lazy"/>

當Defer的控制項被載入的時候才會執行改元件的Loaded的事件並且把Binding的行為建立!

使用 Defer 的XAML Control是降低一開始在VisualTree所建立的物件數量!所以是會加快Xaml的載入速度甚至是可以當作取代Visibility的方式,比如說有設計成Mobile和 Desktop的兩種不同Layout的Xaml會建議使用DeferLoadStrategy或是使用XAML View的方式喔。

這個特性從UWP 10240開始直到15063開始有比較大的變化!先看看Xaml語法的變化如下

<TextBox x:Name="LazyTbx" x:Load="False"/>

這邊沒有看錯從Lazy(字串)變成使用True / False(Boolean)的方式了!所以可以動態的把Xaml element做Unload的操作啦! 

這邊有趣的是 x:Load可以搭配 x:Bind的語法做強型別的方式來操作是否要Load / UnLoad在Xaml的Element而使用x:Load也具有如上的特性

這邊可以看到Live Visual Tree只有Load三個Element

按下CheckBox之後Visual Tree的Element變更,將three的Rectangle作Unload然後把one和two的Rectangle作Load的行為

XAML Code如下

<Page
    x:Class="UIPerformace.Views.ElementXLoadPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UIPerformace.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    DataContext="{x:Bind ElementXLoadPageVM}">

    <Page.Resources>
        <Style TargetType="Rectangle">
            <Setter Property="Width" Value="100"/>
            <Setter Property="Height" Value="100"/>
        </Style>
    </Page.Resources>

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <CheckBox x:Name="DefferCbx" Content="Deffer load with x:Load" Click="{x:Bind ElementXLoadPageVM.DefferLoad}"/>
        <Rectangle x:Name="one" x:Load="{x:Bind ElementXLoadPageVM.IsLoad, Mode=OneWay}" Fill="Red" />
        <Rectangle x:Name="two" x:Load="{x:Bind (x:Boolean)DefferCbx.IsChecked, Mode=OneWay}" Fill="Green"/>
        <Rectangle x:Name="three" x:Load="{x:Bind ElementXLoadPageVM.Reverse(DefferCbx.IsChecked), Mode=OneWay}" Fill="Blue"/>

        <TextBlock x:Name="lazy1" x:Load="False" Text="This is lazy one"/>
    </StackPanel>
</Page>

C# 的ViewModel如下

public class ElementXLoadPageViewModel : BinableBase
    {
        private bool _isLoad;

        public bool IsLoad
        {
            get { return _isLoad; }
            set { SetValue(ref _isLoad, value); }
        }

        public ElementXLoadPageViewModel()
        {
            
        }

        public void DefferLoad(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            IsLoad = !IsLoad;
        }

        public bool Reverse(bool? parameter)
        {
            return !parameter.GetValueOrDefault();
        }
    }

在Unload的Element方式有如下

  1. 使用 x:Bind 的方式傳回 Ture或False。
  2. 在Page或UserControl的Code behind的Code使用UnloadObject。
  3. 使用 Windows.UI.Xaml.Markup.XamlMarkupHelper.UnloadObject 來Unload。

 

參考 Microsoft Docs 聯結有更加完整的說明。


介紹完 x:Load的XAML新語法之後接者是影像處理的部分,在XAML的部分基本上很簡單的做法就是直接使用Image在XAML上並給予ImageSource的方式讓XAML自動作Decode和Render的行為。所以基本上會如下的XAML Code所示

<Page
    x:Class="UIPerformace.Views.ImageLoadPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UIPerformace.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:brushes="using:UIPerformace.Brushes"
    mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Image x:Name="BitmapImg" Stretch="Uniform" Width="256" Height="256">
          <Image.Source>
            <BitmapImage DecodePixelHeight="256" DecodePixelWidth="256" UriSource="ms-appx:///Assets/8K_Image.png"/>
          </Image.Source>
        </Image>
        <Image Stretch="Uniform" Width="256" Height="256" Source="ms-appx:///Assets/aperture-science.svg"/>
        
    </StackPanel>
</Page>
在Creator update的時候直接原生支援SVG的影像格式,可以直接在XAML中直接使用~所以可以盡量避免使用高解析度的bitmap 格式的Image了。 然後依然要注意避免bitmap的image過度Decode的問題,所以就應該要向BitmapImg的寫法在將實際需要decode的Size設定進去來提升影像在載入的效能。

影像載入在UWP上還可以使用更加進階的Win2D + Visual layer來加速Image decode的效能,先來看看如何使用Win2D + Visual layer來載入影像八

先使在Nuget package上面取得 Win2D.uwp 的Package,這邊使用的版本是1.21.0,建立的UWP專案要從10586之後的版本才有支援Visual layer。接者直接來看XAML Code八!

<Page
    x:Class="UIPerformace.Views.ImageLoadPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UIPerformace.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <CheckBox Content="Use composition image" Click="CheckBox_Click"/>
        <Border x:Name="visualBorder" Width="200" Height="200"/>
    </StackPanel>
</Page>

然後是C# Code部分

private async Task ManualDrawImageAsync()
        {
// Step1
            _compositor = ElementCompositionPreview.GetElementVisual(this)?.Compositor;
            //_compositor = Window.Current.Compositor;
            _visual = _compositor?.CreateSpriteVisual();
            _visual.Size = new System.Numerics.Vector2(100.0f, 100.0f);
            ElementCompositionPreview.SetElementChildVisual(visualBorder, _visual);
// Step2
            _canvasDevice = new CanvasDevice();
            _canvasDevice.DeviceLost += _canvasDevice_DeviceLost;
// Step3
            _graphicDevice = CanvasComposition.CreateCompositionGraphicsDevice(_compositor, _canvasDevice);
            _graphicDevice.RenderingDeviceReplaced += _graphicDevice_RenderingDeviceReplaced;
// Step4
            using (var bitmap = await CanvasBitmap.LoadAsync(_canvasDevice, new URI("ms-appx:///Assets/8Kimgage.png")))
            {
                _drawingSurface = _graphicDevice.CreateDrawingSurface(bitmap.Size, Windows.Graphics.DirectX.DirectXPixelFormat.B8G8R8A8UIntNormalized, Windows.Graphics.DirectX.DirectXAlphaMode.Premultiplied);
// Step5
                using (var session = CanvasComposition.CreateDrawingSession(_drawingSurface))
                {
                    session.DrawImage(bitmap);
                }
            }
// Step6
            _visual.Brush = _compositor?.CreateSurfaceBrush(_drawingSurface);
        }

private Compositor _compositor;
private SpriteVisual _visual;
private CanvasDevice _canvasDevice;
private CompositionGraphicsDevice _graphicDevice;
private CompositionDrawingSurface _drawingSurface;

這邊的流程會比較複雜,步驟如下

  1. 先是取得Compositor在Creator update之前需要用ElementCompositionPreview來抓取但是在Creator update可以從Window上面取得(就是註解的那行)接者建立SpriteVisual並且給予Size然後將SpriteVisual放上XAML定義的Border。
  2. 然後建立CanvasDevice,這個物件是在Win2D裡面。主要這個物件是負責建立圖形資源所以一般情況使用 CanvasControl 在XAML的互動。然後這個物件具有處理GPU無法取得的事件( DeviceLost ),在什麼樣的情況會使GPU無法被OS取得像是GPU Driver更新、GPU切換(比如Surface book等裝置具有與CPU整合的GPU和獨立的GPU)。
  3. 然後還要再使用 CanvasComposition來建立GraphicDevice,這個行為會把 UWP的Visual layer和GPU作連結換句話說可以藉此取得GPU裝置的硬體資源,當然這並非是直接存取到GPU。然後還要處理渲染裝置變更的事件 ( RenderingDeviceReplaced )也就是GPU如果變更之後成功切換會觸發~
  4. 接者在建立CanvasBitmap用來產生2D的像素矩陣圖形再用CompositionGraphicDevice來建立CompositionDrawingSurface這個物件就是Direct2D、Direct3D中繪圖會需要使用到的Surface。然後再設定這個Surface的Pixel格式和阿法值的屬性。
  5. 建立繪圖的Session並且把bitmap輸入到該session。
  6. 最後就是使用Compositor產生 Surface的筆刷放到visual裡面。

說真的裡面很多流程和設定值必須瞭解到Direct2D或Direct3D才能比較好理解... 但是在Creator update中有了更加簡單的方式如下C# Code

private void AutoLoadImage()
        {
            _compositor = Window.Current.Compositor;
            _visual = _compositor?.CreateSpriteVisual();
            _visual.Size = new Vector2(100.0f,100.0f);
            ElementCompositionPreview.SetElementChildVisual(visualBorder, _visual);

            var imageSurface = LoadedImageSurface.StartLoadFromUri(new Uri("ms-appx:///Assets/8kimage.png"));
            _visual.Brush = _compositor.CreateSurfaceBrush(imageSurface);
        }

沒看錯只需要用 LoadedImageSurface 這個物件就可以直接建立起 Direct2D的Surface來載入圖片!


最後有介紹到關於Composition API的微調~ 在之前Creator update之前建立Visual layer的物件的時候會必須設定Size(這邊會是Vector2 向量單位)作為讓DX 做Render的依據!但是在Creator update之後加入了兩個新的Property

  • RelativeSizeAdjustment
  • RelativeOffsetAdjustment

這兩個Property是用來與建立Visaul的Parent Object的Size做關聯的屬性,之前在處理SizeChange的時候Visual的變化使用SizeChange的Event來做處理;要不就是使用Expression Animation的方式建立animation來更新Visual的大小。

讓我們直接看看Sample code八

XAML code如下

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid x:Name="rootGrid">
            <Grid.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                    <Setter Property="VerticalAlignment" Value="Center"/>
                </Style>
            </Grid.Resources>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Text="0, 0"/>
            <TextBlock Grid.Row="0" Grid.Column="1" Text="0, 1"/>
            <TextBlock Grid.Row="0" Grid.Column="2" Text="0, 2"/>
            <TextBlock Grid.Row="1" Grid.Column="0" Text="1, 0"/>
            <TextBlock Grid.Row="1" Grid.Column="1" Text="1, 1"/>
            <TextBlock Grid.Row="1" Grid.Column="2" Text="1, 2"/>
            <TextBlock Grid.Row="2" Grid.Column="0" Text="2, 0"/>
            <TextBlock Grid.Row="2" Grid.Column="1" Text="2, 1"/>
            <TextBlock Grid.Row="2" Grid.Column="2" Text="2, 2"/>
        </Grid>
        <StackPanel Grid.Row="1" Orientation="Vertical">
            <Button x:Name="oldBtn" Content="old way" Margin="10" Click="Button_Click"/>
            <Button x:Name="newBtn" Content="new way" Margin="10" Click="Button_Click"/>
        </StackPanel>
    </Grid>

C# code如下

private void CreateRootVisual()
        {
            compositor = Window.Current.Compositor;
            rootVisual = compositor.CreateContainerVisual();
            ElementCompositionPreview.SetElementChildVisual(rootGrid, rootVisual);
            var sizeAnimation = compositor.CreateExpressionAnimation("Vector2(rootGrid.Size.X, rootGrid.Size.Y)");
            sizeAnimation.SetReferenceParameter(nameof(rootGrid), ElementCompositionPreview.GetElementVisual(rootGrid));
            rootVisual.StartAnimation("Size", sizeAnimation);
        }

這邊先做些前置作業,先設定好compositor和rootVisual並且把rootVisual設定到rootGrid(XAML物件)然後建立個sizeAnimation來同步rootVisual和rootGrid的Size!

接著來看看Old school的code八~

private void OldSchool(int i, int j)
        {
            var block = compositor.CreateSpriteVisual();
            block.Opacity = i * opacityRate + j * opacityRate;
            block.Brush = compositor.CreateColorBrush(Windows.UI.Colors.Red);
            var sizeAnimation = compositor.CreateExpressionAnimation($"Vector2(rootVisual.Size.X / {colSize}, rootVisual.Size.Y / {rowSize})");
            sizeAnimation.SetReferenceParameter(nameof(rootVisual), rootVisual);
            var positionAnimation = compositor.CreateExpressionAnimation($"Vector3(block.Size.X * {j}, block.Size.Y * {i}, 0)");
            positionAnimation.SetReferenceParameter(nameof(block), block);

            rootVisual.Children.InsertAtBottom(block);
            block.StartAnimation("Size", sizeAnimation);
            block.StartAnimation("Offset", positionAnimation);
        }

這邊我直接建立的是紅色的 spritevisua,然後建立sizeAnimation、positionAnimation來讓 spriteVisual的Size和Postition offset能夠適應Window SizeChange的狀況。

那麼New school的方式如下Code

private void NewSchool(int i, int j)
        {
            var block = compositor.CreateSpriteVisual();
            block.Opacity = i * opacityRate + j * opacityRate;
            block.Brush = compositor.CreateColorBrush(Windows.UI.Colors.Blue);
            block.RelativeSizeAdjustment = new System.Numerics.Vector2(1.0f / colSize, 1.0f / rowSize);
            block.RelativeOffsetAdjustment = new System.Numerics.Vector3(j * 1.0f / colSize, i * 1.0f / rowSize, 0);
            rootVisual.Children.InsertAtBottom(block);
        }

這邊直接可以設定在RelativeSizeAdjustment和RelativeOffsetAdjustment上!

 


結語

UWP的UI上的特性可以適應不同解析度和不同輸入裝置的特性是最大的特性!如何建立流暢又漂亮的UI需要花時間研究研究阿~

 

 

***以上Code以及說明都有可能隨著Windows 10 的版本以及Visual Studio 版本有所調整!***

參考資料 Microsoft Docs

下次再分享Windows 10 的新技術拉~