Udostępnij przez


Optymalizowanie znaczników XAML

Analizowanie znaczników XAML w celu konstruowania obiektów w pamięci jest czasochłonne dla złożonego interfejsu użytkownika. Poniżej przedstawiono kilka czynności, które można zrobić, aby poprawić czas analizowania i ładowania znaczników XAML oraz wydajność pamięci aplikacji.

Podczas uruchamiania aplikacji ogranicz znaczniki XAML, które są ładowane tylko do tego, czego potrzebujesz dla początkowego interfejsu użytkownika. Sprawdź znaczniki na początkowej stronie (w tym zasoby strony) i upewnij się, że nie są ładowane dodatkowe elementy, które nie są potrzebne od razu. Te elementy mogą pochodzić z wielu źródeł, takich jak słowniki zasobów, elementy, które są początkowo ukryte, i elementy nakładane na inne elementy.

Optymalizacja kodu XAML pod kątem wydajności wymaga wprowadzania kompromisów; nie zawsze istnieje jedno rozwiązanie dla każdej sytuacji. W tym miejscu przyjrzymy się niektórym typowym problemom i przedstawimy wskazówki, których można użyć, aby dokonać odpowiednich kompromisów dla aplikacji.

Minimalizuj liczbę elementów

Mimo że platforma XAML może wyświetlać dużą liczbę elementów, możesz przyspieszyć tworzenie i renderowanie aplikacji przy użyciu najmniejszej liczby elementów w celu uzyskania żądanych wizualizacji.

Wybór sposobu opracowywania kontrolek interfejsu użytkownika będzie mieć wpływ na liczbę elementów interfejsu użytkownika tworzonych podczas uruchamiania aplikacji. Aby uzyskać bardziej szczegółowe informacje na temat optymalizowania układu, zobacz Optymalizowanie układu XAML.

Liczba elementów jest niezwykle ważna w szablonach danych, ponieważ każdy element jest tworzony ponownie dla każdego elementu danych. Aby uzyskać informacje na temat zmniejszania liczby elementów w liście lub siatce, zobacz Zmniejszenie liczby elementów na każdy z nich w artykule Optymalizacja UI dla komponentów ListView i GridView.

W tym miejscu przyjrzymy się innym sposobom zmniejszenia liczby elementów, które aplikacja musi załadować podczas uruchamiania.

Odroczenie tworzenia elementu

Jeśli znacznik XAML zawiera elementy, które nie są wyświetlane od razu, możesz odroczyć ładowanie tych elementów do momentu ich wyświetlenia. Można na przykład opóźnić tworzenie zawartości niewidocznej, takiej jak karta pomocnicza w interfejsie użytkownika przypominającym kartę. Możesz także domyślnie wyświetlać elementy w widoku siatki, ale umożliwić użytkownikowi wyświetlanie danych na liście. Ładowanie listy można opóźnić, dopóki nie będzie potrzebne.

Użyj atrybutu x:Load zamiast właściwości Visibility, aby kontrolować, kiedy element jest wyświetlany. Gdy widoczność elementu jest ustawiona na zwinięte, jest pomijany podczas renderowania, ale nadal ponosisz koszty pamięci związane z instancją obiektu. Jeśli zamiast tego używasz biblioteki x:Load, platforma nie tworzy wystąpienia obiektu, dopóki nie będzie potrzebne, więc koszty pamięci są jeszcze niższe. Wadą jest konieczność poniesienia niewielkiego narzutu na pamięć (około 600 bajtów), kiedy interfejs użytkownika nie jest wczytany.

Uwaga / Notatka

Możesz opóźnić ładowanie elementów, używając atrybutu x:Load lub x:DeferLoadStrategy. Atrybut x:Load jest dostępny od wersji Windows 10 Creators Update (wersja 1703, kompilacja zestawu SDK 15063). Aby móc użyć x:Load, minimalna wersja przeznaczona dla projektu programu Visual Studio musi być Windows 10 Creators Update (10.0, Kompilacja 15063). Aby kierować do wcześniejszych wersji, użyj polecenia x:DeferLoadStrategy.

W poniższych przykładach pokazano różnicę w liczbie elementów i użyciu pamięci, gdy są używane różne techniki do ukrywania elementów interfejsu użytkownika. Kontrolki ListView i GridView, które zawierają identyczne elementy, są umieszczone w głównej siatce źródłowej strony. ListView nie jest widoczny, ale GridView jest wyświetlany. Kod XAML w każdym z tych przykładów tworzy ten sam interfejs użytkownika na ekranie. Do profilowania i wydajności używamy narzędzi programu Visual Studio, aby sprawdzić liczbę elementów i użycie pamięci.

Opcja 1 — nieefektywne

W tym miejscu element ListView jest ładowany, ale nie jest widoczny, ponieważ jego szerokość wynosi 0. Element ListView i każdy z jego elementów podrzędnych są tworzone w drzewie wizualizacji i ładowane do pamięci.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE.-->
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <ListView x:Name="List1" Width="0">
        <ListViewItem>Item 1</ListViewItem>
        <ListViewItem>Item 2</ListViewItem>
        <ListViewItem>Item 3</ListViewItem>
        <ListViewItem>Item 4</ListViewItem>
        <ListViewItem>Item 5</ListViewItem>
        <ListViewItem>Item 6</ListViewItem>
        <ListViewItem>Item 7</ListViewItem>
        <ListViewItem>Item 8</ListViewItem>
        <ListViewItem>Item 9</ListViewItem>
        <ListViewItem>Item 10</ListViewItem>
    </ListView>

    <GridView x:Name="Grid1">
        <GridViewItem>Item 1</GridViewItem>
        <GridViewItem>Item 2</GridViewItem>
        <GridViewItem>Item 3</GridViewItem>
        <GridViewItem>Item 4</GridViewItem>
        <GridViewItem>Item 5</GridViewItem>
        <GridViewItem>Item 6</GridViewItem>
        <GridViewItem>Item 7</GridViewItem>
        <GridViewItem>Item 8</GridViewItem>
        <GridViewItem>Item 9</GridViewItem>
        <GridViewItem>Item 10</GridViewItem>
    </GridView>
</Grid>

Aktywne drzewo wizualne z załadowanym ListView. Łączna liczba elementów dla strony wynosi 89.

Zrzut ekranu przedstawiający drzewo wizualizacji z widokiem listy.

Element ListView i jego elementy podrzędne są ładowane do pamięci.

Zrzut ekranu przedstawiający tabelę Test pamięci zarządzanej 1 dot E X E pokazującą ListView i jej elementy podrzędne ładowane do pamięci.

Opcja 2 — lepsza

W tym miejscu widoczność elementu ListView jest ustawiona na zwinięcie (drugi kod XAML jest identyczny z oryginalnym). Element ListView jest tworzony w drzewie wizualizacji, ale jego elementy podrzędne nie są. Jednak są one ładowane do pamięci, więc użycie pamięci jest identyczne z poprzednim przykładem.

<ListView x:Name="List1" Visibility="Collapsed">

Dynamiczne drzewo wizualizacji z zwiniętym elementem ListView. Łączna liczba elementów dla strony to 46.

Zrzut ekranu przedstawiający drzewo wizualizacji z zwiniętym widokiem listy.

Element ListView i jego elementy podrzędne są ładowane do pamięci.

Zaktualizowany zrzut ekranu przedstawiający tabelę Managed Memory Test App 1.exe, pokazującą, że ListView i jej elementy podrzędne są załadowane do pamięci.

Opcja 3 — najbardziej wydajna

W tym miejscu element ListView ma atrybut x:Load ustawiony na wartość False (drugi kod XAML jest identyczny z oryginałem). Element ListView nie jest tworzony w drzewie wizualizacji ani ładowany do pamięci podczas uruchamiania.

<ListView x:Name="List1" Visibility="Collapsed" x:Load="False">

Dynamiczne drzewo wizualizacji z nie załadowanym elementem ListView. Łączna liczba elementów dla strony to 45.

drzewo wizualne z niezaładowanym widokiem listy

Element ListView i jego elementy podrzędne nie są ładowane do pamięci.

drzewo wizualne z widokiem listy

Uwaga / Notatka

Liczba elementów i użycie pamięci w tych przykładach są bardzo małe i są wyświetlane tylko w celu zademonstrowania koncepcji. W tych przykładach obciążenie związane z używaniem funkcji x:Load jest większe niż oszczędności pamięci, więc aplikacja nie przyniesie korzyści. Użyj narzędzi profilowania w aplikacji, aby określić, czy aplikacja będzie korzystać z odroczonego ładowania.

Korzystanie z właściwości panelu układu

Panele układu mają właściwość Tło, więc nie ma potrzeby umieszczania prostokąta przed panelem, aby go pokolorować.

nieefektywne

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid>
    <Rectangle Fill="Black"/>
</Grid>

wydajne

<Grid Background="Black"/>

Panele układu mają również wbudowane właściwości obramowania, więc nie trzeba umieszczać elementu Obramowanie wokół panelu układu. Zobacz Optymalizowanie układu XAML, aby uzyskać więcej informacji i przykładów.

Używanie obrazów zamiast elementów wektorowych

Jeśli używasz tego samego elementu opartego na wektorach wystarczająco dużo razy, bardziej wydajne staje się użycie elementu Image. Elementy oparte na wektorach mogą być droższe, ponieważ procesor CPU musi utworzyć poszczególne elementy oddzielnie. Plik obrazu musi być zdekodowany tylko raz.

Optymalizowanie zasobów i słowników zasobów

Zwykle używasz słowników zasobów do przechowywania na nieco globalnym poziomie zasobów, do których chcesz się odwoływać w wielu miejscach w aplikacji. Na przykład style, pędzle, szablony itd.

Ogólnie rzecz biorąc, zoptymalizowaliśmy ResourceDictionary, aby nie tworzyć zasobów, chyba że zostaną o nie poproszone. Jednak istnieją sytuacje, których należy unikać, aby zasoby nie były tworzone niepotrzebnie.

Zasoby z wartością x:Name

Użyj atrybutu x:Key, aby odwołać się do zasobów. Żaden zasób z atrybutem x:Name nie będzie korzystał z optymalizacji platformy; zamiast tego jest tworzony natychmiast po utworzeniu elementu ResourceDictionary. Dzieje się tak, ponieważ x:Name informuje platformę, że aplikacja potrzebuje dostępu do tego zasobu na poziomie pola, dlatego platforma musi utworzyć coś, do czego można odwołać się.

ResourceDictionary w kontrolce użytkownika

Element ResourceDictionary zdefiniowany wewnątrz UserControl ponosi karę. Platforma tworzy kopię obiektu ResourceDictionary dla każdego wystąpienia obiektu UserControl. Jeśli masz dużo używanego elementu UserControl, przenieś element ResourceDictionary z elementu UserControl i umieść go na poziomie strony.

Zakres zasobu i elementu ResourceDictionary

Jeśli strona odwołuje się do kontrolki użytkownika lub zasobu zdefiniowanego w innym pliku, platforma również analizuje ten plik.

W tym miejscu, ponieważ InitialPage.xaml używa jednego zasobu z ExampleResourceDictionary.xaml, cała ExampleResourceDictionary.xaml musi zostać przeanalizowana podczas uruchamiania.

InitialPage.xaml.

<Page x:Class="ExampleNamespace.InitialPage" ...>
    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="ExampleResourceDictionary.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Page.Resources>

    <Grid>
        <TextBox Foreground="{StaticResource TextBrush}"/>
    </Grid>
</Page>

ExampleResourceDictionary.xaml.

<ResourceDictionary>
    <SolidColorBrush x:Key="TextBrush" Color="#FF3F42CC"/>

    <!--This ResourceDictionary contains many other resources that
        are used in the app, but are not needed during startup.-->
</ResourceDictionary>

Jeśli używasz zasobu na wielu stronach w całej aplikacji, przechowywanie go w App.xaml jest dobrym rozwiązaniem i unika duplikowania. Jednak App.xaml jest parsowana podczas uruchamiania aplikacji, więc każdy zasób, który jest używany tylko na jednej stronie (chyba że ta strona jest stroną początkową), powinien zostać umieszczony w zasobach lokalnych strony. W tym przykładzie przedstawiono App.xaml zawierające zasoby używane tylko przez jedną stronę, która nie jest stroną początkową. To niepotrzebnie zwiększa czas uruchamiania aplikacji.

app.xaml

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Application ...>
     <Application.Resources>
        <SolidColorBrush x:Key="DefaultAppTextBrush" Color="#FF3F42CC"/>
        <SolidColorBrush x:Key="InitialPageTextBrush" Color="#FF3F42CC"/>
        <SolidColorBrush x:Key="SecondPageTextBrush" Color="#FF3F42CC"/>
        <SolidColorBrush x:Key="ThirdPageTextBrush" Color="#FF3F42CC"/>
    </Application.Resources>
</Application>

InitialPage.xaml.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page x:Class="ExampleNamespace.InitialPage" ...>
    <StackPanel>
        <TextBox Foreground="{StaticResource InitialPageTextBrush}"/>
    </StackPanel>
</Page>

SecondPage.xaml.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page x:Class="ExampleNamespace.SecondPage" ...>
    <StackPanel>
        <Button Content="Submit" Foreground="{StaticResource SecondPageTextBrush}"/>
    </StackPanel>
</Page>

Aby ten przykład był bardziej wydajny, przenieś SecondPageTextBrush do SecondPage.xaml i przenieś ThirdPageTextBrush do ThirdPage.xaml. InitialPageTextBrush może pozostać w App.xaml, ponieważ zasoby aplikacji muszą być analizowane podczas uruchamiania aplikacji w każdym razie.

Skonsoliduj wiele pędzli, które wyglądają tak samo, w jedną zasobę

Platforma XAML próbuje buforować często używane obiekty, aby można było ich używać tak często, jak to możliwe. Ale XAML nie może łatwo rozpoznać, czy pędzel zadeklarowany w jednym fragmencie kodu znaczników jest taki sam jak pędzel zadeklarowany w innym. W tym przykładzie użyto SolidColorBrush do zademonstrowania, ale przypadek jest bardziej prawdopodobny i ważniejszy w przypadku GradientBrush. Sprawdź również, czy pędzle używają wstępnie zdefiniowanych kolorów; na przykład "Orange" i "#FFFFA500" są tym samym kolorem.

nieefektywne.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Page ... >
    <StackPanel>
        <TextBlock>
            <TextBlock.Foreground>
                <SolidColorBrush Color="#FFFFA500"/>
            </TextBlock.Foreground>
        </TextBlock>
        <Button Content="Submit">
            <Button.Foreground>
                <SolidColorBrush Color="#FFFFA500"/>
            </Button.Foreground>
        </Button>
    </StackPanel>
</Page>

Aby naprawić duplikację, zdefiniuj szczotkę jako zasób. Jeśli kontrolki na innych stronach używają tego samego pędzla, przenieś ją do App.xaml.

wydajne.

<Page ... >
    <Page.Resources>
        <SolidColorBrush x:Key="BrandBrush" Color="#FFFFA500"/>
    </Page.Resources>

    <StackPanel>
        <TextBlock Foreground="{StaticResource BrandBrush}" />
        <Button Content="Submit" Foreground="{StaticResource BrandBrush}" />
    </StackPanel>
</Page>

Minimalizowanie przerysowania

Przerysowywanie występuje, gdy więcej niż jeden obiekt jest rysowany w tych samych pikselach ekranu. Należy pamiętać, że czasami występuje kompromis między tym wskazówkami a pragnieniem zminimalizowania liczby elementów.

Użyj DebugSettings.IsOverdrawHeatMapEnabled jako diagnostyki wizualnej. Możesz znaleźć narysowane obiekty, których nie wiedziałeś, że były w scenie.

Przezroczyste lub ukryte elementy

Jeśli element nie jest widoczny, ponieważ jest przezroczysty lub ukryty za innymi elementami i nie przyczynia się do układu, usuń go. Jeśli element nie jest widoczny w początkowym stanie wizualnym, ale jest widoczny w innych stanach wizualnych, użyj x:Load, aby kontrolować jego stan, lub ustaw Widoczność na Zwinięty na elemencie i zmień wartość na Widoczny w odpowiednich stanach. Wystąpią wyjątki od tej heurystyki: ogólnie rzecz biorąc, wartość, którą właściwość ma w większości stanów wizualnych, najlepiej ustawiać lokalnie na elemecie.

Elementy złożone

Użyj elementu złożonego zamiast warstwowania wielu elementów, aby utworzyć efekt. W tym przykładzie wynikiem jest dwustonowany kształt, w którym górna połowa jest (z tła Grid), a dolna połowa jest szara (z półprzezroczystej białej prostokąta alfa-blended na czarnym tle Grid). W tym miejscu wypełniane są 150% pikseli niezbędnych do osiągnięcia wyniku.

nieefektywne.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid Background="Black">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Rectangle Grid.Row="1" Fill="White" Opacity=".5"/>
</Grid>

wydajne.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Rectangle Fill="Black"/>
    <Rectangle Grid.Row="1" Fill="#FF7F7F7F"/>
</Grid>

Panele układu

Panel układu może mieć dwa cele: nadanie koloru obszarowi i rozmieszczenie elementów podrzędnych. Jeśli element dalej w kolejności z jest już kolorowanie obszaru, panel układu przed nie musi malować tego obszaru: zamiast tego może po prostu skupić się na układaniu swoich dzieci. Oto przykład.

nieefektywne.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<GridView Background="Blue">
    <GridView.ItemTemplate>
        <DataTemplate>
            <Grid Background="Blue"/>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

wydajne.

<GridView Background="Blue">
    <GridView.ItemTemplate>
        <DataTemplate>
            <Grid/>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

Jeśli Grid musi być testowalny, ustaw na nim wartość tła przezroczystą.

Granice

Użyj elementu Border, aby narysować obramowanie wokół obiektu. W tym przykładzie siatki jest używana jako prowizoryczny obramowanie wokół TextBox. Ale wszystkie piksele w komórce środkowej są przerysowane.

nieefektywne.

<!-- NOTE: EXAMPLE OF INEFFICIENT CODE; DO NOT COPY-PASTE. -->
<Grid Background="Blue" Width="300" Height="45">
    <Grid.RowDefinitions>
        <RowDefinition Height="5"/>
        <RowDefinition/>
        <RowDefinition Height="5"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="5"/>
        <ColumnDefinition/>
        <ColumnDefinition Width="5"/>
    </Grid.ColumnDefinitions>
    <TextBox Grid.Row="1" Grid.Column="1"></TextBox>
</Grid>

wydajne.

<Border BorderBrush="Blue" BorderThickness="5" Width="300" Height="45">
    <TextBox/>
</Border>

Margines

Należy pamiętać o marginesach. Dwa sąsiednie elementy nakładają się (prawdopodobnie przypadkowo), jeśli marginesy ujemne rozszerzają się na granice renderowania innego elementu i powodują nadmierne nakładanie się.

Buforowanie zawartości statycznej

Innym źródłem przerysowania jest kształt wykonany z wielu nakładających się elementów. Jeśli ustawisz CacheMode na BitmapCache na UIElement, który zawiera kształt złożony, platforma renderuje element jako mapę bitową raz, i następnie używa tej mapy bitowej w każdej klatce zamiast przerysowania.

nieefektywne.

<Canvas Background="White">
    <Ellipse Height="40" Width="40" Fill="Blue"/>
    <Ellipse Canvas.Left="21" Height="40" Width="40" Fill="Blue"/>
    <Ellipse Canvas.Top="13" Canvas.Left="10" Height="40" Width="40" Fill="Blue"/>
</Canvas>

diagram Venna z trzema solidnymi okręgami

Powyższy obraz jest wynikiem, ale oto mapa obszarów z nadmiernym rysowaniem. Ciemniejszy odcień czerwonego wskazuje większe ilości przekroczenia.

diagram Venna przedstawiający nakładające się obszary

wydajne.

<Canvas Background="White" CacheMode="BitmapCache">
    <Ellipse Height="40" Width="40" Fill="Blue"/>
    <Ellipse Canvas.Left="21" Height="40" Width="40" Fill="Blue"/>
    <Ellipse Canvas.Top="13" Canvas.Left="10" Height="40" Width="40" Fill="Blue"/>
</Canvas>

Zwróć uwagę na użycie CacheMode. Nie używaj tej techniki, jeśli którykolwiek z kształtów podrzędnych jest animowany, ponieważ pamięć podręczna mapy bitowej prawdopodobnie będzie musiała zostać ponownie wygenerowana dla każdej klatki, niwecząc jego celowość.

Korzystanie z XBF2

XBF2 to binarna reprezentacja znaczników XAML, która pozwala uniknąć wszystkich kosztów analizowania tekstu w czasie wykonywania. Optymalizuje również dane binarne pod kątem ładowania i tworzenia drzewa oraz umożliwia "szybką ścieżkę" dla typów XAML w celu poprawy kosztów tworzenia obiektów, na przykład VSM, ResourceDictionary, Style itd. Jest całkowicie mapowany w pamięci, dzięki czemu nie ma potrzeby wykorzystywania stosu do ładowania i odczytywania strony XAML. Ponadto zmniejsza to zużycie dysku przechowywanych stron XAML w aplikacji. XBF2 to bardziej kompaktowa reprezentacja i może zmniejszyć rozmiar dysku porównawczych plików XAML/XBF1 o maksymalnie 50%. Na przykład wbudowana aplikacja Zdjęcia widziała około 60% redukcji po konwersji na XBF2 spada z około około 1 mb zasobów XBF1 do ~400 kb zasobów XBF2. Widzieliśmy również, że aplikacje osiągają korzyści w zakresie od 15 do 20% na CPU i od 10 do 15% na stosie Win32.

Wbudowane kontrolki i słowniki XAML dostarczane przez platformę są już w pełni zgodne z XBF2. W przypadku własnej aplikacji upewnij się, że plik projektu deklaruje wartość TargetPlatformVersion 8.2 lub nowszą.

Aby sprawdzić, czy masz XBF2, otwórz aplikację w edytorze binarnym; 12 i 13 bajtów to 00 02, jeśli masz XBF2.