Поделиться через


Оптимизация разметки XAML

Анализ разметки XAML для создания объектов в памяти занимает много времени для сложного пользовательского интерфейса. Вот что можно сделать, чтобы улучшить анализ и время загрузки разметки XAML, а также повысить эффективность работы приложения с памятью.

При запуске приложения ограничьте разметку XAML, загруженную только тем, что вам нужно для начального пользовательского интерфейса. Проверьте разметку на начальной странице (включая ресурсы страницы) и убедитесь, что вы не загружаете лишние элементы, которые сейчас не нужны. Эти элементы могут поступать из различных источников, таких как словари ресурсов, элементы, которые изначально свернуты, и элементы, нарисованные поверх других элементов.

Оптимизация кода XAML для повышения эффективности требует определенных компромиссов; не для всех ситуаций существуют универсальные решения. Здесь мы рассмотрим некоторые распространенные проблемы и дадим рекомендации по выбору правильных компромиссов для вашего приложения.

Свести к минимуму число элементов

Несмотря на то, что платформа XAML может отображать большое количество элементов, вы можете сделать приложение выложенным и отрисовывать быстрее, используя наименьшее количество элементов для достижения нужных визуальных элементов.

Принимаемые вами решения о расположении элементов управления пользовательского интерфейса повлияют на количество создаваемых при запуске приложения элементов пользовательского интерфейса. Более подробные сведения об оптимизации макета см. в разделе Оптимизация макета XAML.

Число элементов очень важно в шаблонах данных, так как каждый элемент создается заново для каждого элемента данных. Сведения об уменьшении числа элементов в списке или сетке см. в разделе Уменьшение элементов для каждого элемента статьи Оптимизация пользовательского интерфейса ListView и GridView.

Здесь мы рассмотрим некоторые другие способы уменьшения количества загружаемых при запуске приложения элементов.

Отложенное создание элементов

Если разметка XAML содержит элементы, которые не отображаются сразу, можно отложить загрузку этих элементов до тех пор, пока не возникнет необходимость их отобразить. Например, можно отсрочить создание невидимого содержимого (например, дополнительной вкладки в пользовательском интерфейсе). Или можно отображать элементы в представлении сетки по умолчанию, однако предоставить пользователю возможность просмотра данных списком. Можно отложить загрузку списка до тех пор, пока он не понадобится.

Используйте атрибут x:Load вместо свойства Visibility, чтобы определить, когда будет отображаться элемент. Если для видимости элемента задано свойство Collapsed, он будет пропущен во время прохода отрисовки, но экземпляр объекта все равно будет потреблять ресурсы памяти. Когда используется x:Load, экземпляр объекта не создается платформой до тех пор, пока он не потребуется, поэтому потребляется еще меньше памяти. Недостаток этот метода заключается в небольшой перегрузке памяти (около 600 байтов), когда пользовательский интерфейс не загружается.

Примечание.

Загрузку элементов можно отложить с помощью атрибута x:Load или x:DeferLoadStrategy. Атрибут x:Load доступен, начиная с Windows 10 Creators Update (версия 1703, сборка пакета SDK 15063). Для использования атрибута x:Load минимальной версией, указанной в вашем проекте Visual Studio, должна быть Windows 10 Creators Update (10.0, сборка 15063). Для более ранних версий используйте x:DeferLoadStrategy.

В следующих примерах показано различие в количестве элементов и использовании памяти при использовании разных техник скрытия элементов пользовательского интерфейса. ListView и GridView, содержащие идентичные элементы, помещаются в корневой элемент Grid страницы. ListView не отображается, а GridView отображается. Код XAML в каждом из этих примеров создает один и тот же интерфейс на экране. Мы используем инструменты профилирования и контроля производительности Visual Studio, чтобы проверить количество элементов и использование памяти.

Вариант 1 — неэффективный

Здесь ListView загружается, но не отображается, так как его ширина равна 0. ListView и все его дочерние элементы создаются в визуальном дереве и загружаются в память.

<!-- 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>

Динамическое визуальное дерево с загруженным элементом управления ListView. Общее количество элементов на странице — 89.

Снимок экрана: визуальное дерево с ListView.

ListView и его дочерние элементы загружаются в память.

Снимок экрана: таблица управляемой памяти (TestApp1.exe), в которой отображается ListView с дочерними элементами, загружаемыми в память.

Вариант 2 — лучше

Здесь свойство Visibility ListView имеет значение Collapsed (прочий код XAML совпадает с первоначальным кодом). ListView создается в визуальном дереве, но его дочерние элементы — нет. Тем не менее они загружаются в память, поэтому использование памяти идентично предыдущему примеру.

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

Динамическое визуальное дерево со свернутым элементом управления ListView. Общее количество элементов на странице — 46.

Снимок экрана: визуальное дерево со свернутым элементом управления ListView.

ListView и его дочерние элементы загружаются в память.

Обновленный снимок экрана: таблица управляемой памяти (TestApp1.exe), в которой отображается ListView с дочерними элементами, загружаемыми в память.

Вариант 3 — наиболее эффективен

Здесь ListView имеет значение атрибута x:Load False (прочий код XAML совпадает с первоначальным кодом). ListView не создается в визуальном дереве и не загружается в память при запуске.

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

Динамическое визуальное дерево с незагруженным элементом управления ListView. Общее количество элементов на странице — 45.

Визуальное дерево с незагруженным представлением списка

ListView и его дочерние элементы не загружаются в память.

Визуальное дерево с представлением списка

Примечание.

Количество элементов и использование памяти в этих примерах очень малы и показаны только для того, чтобы продемонстрировать концепцию. В этих примерах расходы на использование x:Load выше, чем экономия памяти, поэтому приложение ничего не выигрывает. Следует использовать со своим приложением инструменты профилирования, чтобы установить, будет ли приложению польза от отложенной загрузки.

Использование свойств панели макета

У панелей макета есть свойство Background, поэтому нет необходимости использовать перед ними класс Rectangle, только чтобы изменить цвет панели.

Неэффективно

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

Эффективно

<Grid Background="Black"/>

Панели макета также имеют встроенные свойства границ, поэтому вокруг панели макета не нужно помещать элемент Border. Дополнительные сведения и примеры доступны в разделе Оптимизация макета XAML.

Использование изображений вместо векторных элементов

Если один и тот же элемент на основе векторов используется несколько раз, эффективней будет использовать вместо него элемент Image. Элементы на основе векторов могут быть дороже, так как ЦП должен создавать каждый отдельный элемент отдельно. Файл изображения должен быть декодирован только один раз.

Оптимизация ресурсов и словарей ресурсов

Как правило, словари ресурсов используются для сохранения на определенном глобальном уровне ресурсов, на которые вы хотите ссылаться в нескольких местах вашего приложения. Например, стили, кисти, шаблоны и т. д.

В целом мы оптимизировали ResourceDictionaries так, чтобы экземпляр ресурса не создавался без соответствующего запроса. Однако есть ситуации, которых следует избегать, чтобы не создавать экземпляры ресурсов без лишней необходимости.

Ресурсы с x:Name

Используйте атрибут x:Key для создания ссылки на ресурсы. Ресурсы с атрибутом x:Name не получают преимуществ при оптимизации платформы. Вместо этого экземпляр ресурса создается сразу же после создания ResourceDictionary. Это происходит из-за того, что x:Name сообщает платформе, что приложению требуется доступ к этому ресурсу, поэтому платформа должна создать ссылку на нее.

ResourceDictionary в UserControl

Определение ResourceDictionary в UserControl ведет к снижению производительности. Платформа создает копию такого ResourceDictionary для каждого экземпляра UserControl. Если UserControl используется интенсивно, извлеките ResourceDictionary из UserControl и разместите его на уровне страницы.

Ресурс и область ResourceDictionary

Если страница ссылается на пользовательский элемент управления или ресурс, определенный в другом файле, платформа также анализирует этот файл.

Поскольку в этом примере InitialPage.xaml использует один ресурс из ExampleResourceDictionary.xaml, последний необходимо проанализировать при запуске.

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>

Если ресурс используется на нескольких страницах приложения, мы рекомендуем сохранить его в файл App.xaml, чтобы избежать дублирования. Но файл App.xaml анализируется при запуске приложения, поэтому любой ресурс, используемый только на одной странице (кроме начальной), необходимо включить в локальные ресурсы такой страницы. В этом примере неправильного использования файл App.xaml содержит ресурсы, используемые только одной страницей, которая не является начальной. Это не обязательно увеличивает время запуска приложения.

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>

Чтобы сделать сценарий в этом примере более эффективным, нужно переместить файл SecondPageTextBrush в SecondPage.xaml, а ThirdPageTextBrush — в ThirdPage.xaml. InitialPageTextBrush можно оставить в файле App.xaml, потому что ресурсы приложения необходимо проанализировать при запуске в любом случае.

Консолидация нескольких кистей, которые выглядят одинаково в одном ресурсе

Платформа XAML пытается кэшировать часто используемые объекты, чтобы их можно было повторно использовать как можно чаще. Но XAML не может легко определить, объявленная кистью в одной части разметки, совпадает с кистью, объявленной в другом. В этом примере класс SolidColorBrush использован для демонстрации, но гораздо важнее применять этот способ к классу GradientBrush. Также проверьте кисти, использующие предопределенные цвета, например "Orange" и "#FFFFA500" — это один и тот же цвет.

Неэффективно

<!-- 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>

Чтобы исправить дублирование, определите кисть как ресурс. Если элементы управления на других страницах используют эту же кисть, переместите ее в файл App.xaml.

Эффективно

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

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

Свести к минимуму переполнение

Перерисовка происходит, когда одни и те же пиксели экрана используются для отрисовки нескольких объектов. Обратите внимание, что иногда существует компромисс между этим руководством и желанием свести к минимуму количество элементов.

Используйте DebugSettings.IsOverdrawHeatMapEnabled в качестве визуальной диагностики. Так вы можете обнаружить в сцене отрисованные объекты, о наличии которых не знали.

Прозрачные или скрытые элементы

Если элемент не отображается, так как он прозрачный или скрытый за другими элементами, и он не вносит вклад в макет, а затем удалите его. Если элемент не отображается в первоначальном визуальном состоянии, но отображается в других визуальных состояниях, отрегулируйте его состояние с помощью x:Load или присвойте свойству Visibility для элемента значение Collapsed и измените значение свойства на Visible для соответствующих состояний. У этой эвристической процедуры могут быть исключения: значение свойства в большинстве визуальных состояний лучше присваивать элементу локально.

Составные элементы

Используйте составной элемент вместо слоя нескольких элементов для создания эффекта. Результат выполнения этого примера — форма, закрашенная двумя цветами. Верхняя часть черного цвета (полученного присвоением соответствующего фона объекту класса Grid), а нижняя серого (полученного наложением объекта Rectangle полупрозрачного белого цвета в альфа-канале на объект Grid с черным фоном). Здесь заполняется 150 % пикселей, необходимых для достижения результата.

Неэффективно

<!-- 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>

Эффективно

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

Панели макета

Панель макета может иметь два назначения: цвет области и выделение дочерних элементов. Если элемент еще больше назад в z-порядке уже цвет области, то панель макета перед ней не нужно красить эту область: вместо этого она может просто сосредоточиться на расположении своих дочерних элементов. Рассмотрим пример.

Неэффективно

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

Эффективно

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

Если следует выполнить проверку нажатия объекта класса Grid, присвойте ему прозрачный фон.

Границы

Используйте элемент Border для рисования границы вокруг объекта. В этом примере объект класса Grid используется в качестве временной границы вокруг объекта класса TextBox. Но все пиксели в центральной ячейке переопределяются.

Неэффективно

<!-- 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>

Эффективно

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

Отступы

Помните о полях. Два соседних элемента будут перекрываться (возможно, случайно), если отрицательные поля расширяются в границы отрисовки другого и вызывают перезарисовку.

Кэширование статического содержимого

Другой источник перезарисовки — это форма, созданная из множества перекрывающихся элементов. Если вы присвоили CacheMode значение BitmapCache в классе UIElement, который содержит составную форму, платформа обработает элемент как растровое изображение, а затем будет использовать это изображение в каждом кадре вместо того, чтобы перерисовывать объекты.

Неэффективно

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

Схема Венна с тремя твердыми кругами

Приведенное выше изображение является результатом, но вот карта переопределенных регионов. Темный красный цвет указывает на более высокие объемы передравов.

Схема Венна с перекрывающимися областями

Эффективно

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

Обратите внимание на использование класса CacheMode. Не используйте этот метод, если какие-либо из вложенных фигур анимируются, так как кэш растровых изображений, скорее всего, потребуется повторно создать каждый кадр, победив цель.

Использование XBF2

XBF2 — это двоичное представление разметки XAML, которая позволяет избежать всех затрат на анализ текста во время выполнения. Он также оптимизирует двоичный файл для создания нагрузки и дерева и позволяет "быстро путь" для типов XAML повысить затраты на создание куч и объектов, например VSM, ResourceDictionary, Стили и т. д. Он полностью сопоставлен с памятью, поэтому нет кучи для загрузки и чтения страницы XAML. Кроме того, это сокращает объем дискового пространства хранимых страниц XAML в appx. XBF2 — это более компактное представление, и это может сократить объем дискового пространства сравнительного файла XAML/XBF1 до 50 %. Например, встроенное приложение "Фотографии" сократилось примерно на 60 % после преобразования в XBF2 с около 1 мб активов XBF1 до ~400 КБ активов XBF2. Мы также видели преимущества приложений в любом месте от 15 до 20% в ЦП и 10 до 15% в куче Win32.

Встроенные элементы управления и словари XAML, которые предоставляет платформа, уже полностью включены в XBF2. Для собственного приложения убедитесь, что файл проекта объявляет TargetPlatformVersion 8.2 или более поздней версии.

Чтобы проверить наличие XBF2, откройте приложение в двоичном редакторе; 12-й и 13-й байты — 00 02, если у вас есть XBF2.