Рекомендации по повышению производительности запуска приложения
Создавайте приложения универсальной платформы Windows (UWP) с оптимальными временем запуска, улучшив управление им, а также активацией.
Рекомендации по повышению производительности запуска приложения
Восприятие пользователями вашего приложения как быстрого или медленного отчасти зависит от того, сколько времени ему требуется для запуска. Под временем запуска в этой статье подразумевается время с момента запуска приложения и до момента, когда пользователь получает возможность полноценно с ним взаимодействовать. В этой статье приводятся предложения по улучшению производительности приложения при запуске.
Измерение времени запуска приложенияПрежде чем приступить к фактическому измерению времени запуска приложения, запустите его несколько раз. Это даст вам некую отправную точку для измерения и обеспечит максимально короткое время запуска.
Приложение UWP попадает на компьютеры ваших клиентов скомпилированным с помощью цепочки инструментов .NET Native. .NET Native — это современная технология компиляции, которая преобразует MSIL во встроенный готовый к запуску машинный код. Приложения .NET Native запускаются быстрее, используют меньше памяти и меньше заряда батареи, чем аналоги MSIL. Приложения, созданные с помощью .NET Native, статически связываются в особой среде выполнения и новом едином ядре .NET, которые могут работать на всех устройствах и поэтому не зависят от реализации .NET. На компьютере разработчика приложение по умолчанию использует .NET Native, если его построение выполняется в режиме выпуска, и использует CoreCLR, если его построение выполняется в режиме отладки. Вы можете настроить этот параметр в Visual Studio на странице сборки в разделе "Свойства" (C#) или в разделе "Компиляция" -> "Расширенные" в "Мой проект" (VB). Найдите флажок «Компилировать с помощью собственной цепочки инструментов .NET».
Необходимо выполнить измерения, отражающие реальную картину взаимодействия пользователя с приложением. Поэтому, если вы не уверены, что компилируете свое приложение в машинный код на своем компьютере разработчика, можно запустить средство собственного генератора изображений (Ngen.exe) для предварительной компиляции приложения перед измерением времени запуска.
Следующая процедура описывает запуск Ngen.exe для предварительной компиляции приложения.
Запуск Ngen.exe
Запустите свое приложение хотя бы один раз, чтобы убедиться, что Ngen.exe его обнаруживает.
Откройте планировщик заданий одним из следующих способов.
- Выполните поиск планировщика заданий с начального экрана.
- Запустите файл taskschd.msc.
На левой панели планировщика заданий разверните библиотеку планировщика заданий.
Разверните элемент Microsoft.
Разверните элемент Windows.
Выберите .NET Framework.
Выберите из списка задач .NET Framework NGEN 4.x.
Если вы используете 64-разрядный компьютер, будет доступен также элемент .NET Framework NGEN v4.x 64. Если вы собираете 64-разрядное приложение, выберите .NET Framework NGEN v4.x 64.
В меню Действие нажмите Выполнить.
Ngen.exe выполнит предварительную компиляцию всех приложений на компьютере, которые были использованы и не имеют машинных образов. Выполнение предварительной компиляции для большого количества приложений может потребовать много времени, зато в следующий раз она будет проведена гораздо быстрее.
При перекомпиляции вашего приложения машинный образ уже не используется. Вместо этого приложение компилируется "на лету", то есть компиляция происходит при запуске приложения. Вы должны повторно запустить Ngen.exe, чтобы получить новый машинный образ.
Максимально возможное оттягивание выполнения работыЧтобы уменьшить время запуска приложения, выполняйте только самые необходимые действия, обеспечивающие пользователю возможность приступить к работе с приложением. Это может оказаться особенно полезным, если удастся отложить загрузку дополнительных сборок. Среда CLR загружает сборку при первом использовании. Если свести к минимуму количество загружаемых сборок, скорее всего, удастся сократить время запуска приложения и оптимизировать потребление памяти.
Независимое выполнение долгосрочных работПриложение может реагировать на действия пользователя, даже если не все его части полностью функциональны. Например, если приложение отображает данные, на получение которых требуется время, вы можете сделать выполнение этого кода независимым от выполнения кода запуска приложения за счет асинхронного получения данных. Как только данные будут доступны, заполните ими пользовательский интерфейс приложения.
Большинство API универсальной платформы Windows (UWP), обеспечивающих получение данных, являются асинхронными, поэтому получение данных в вашем приложении, скорее всего, тоже будет асинхронным. Подробнее об асинхронных API см. в разделе Вызов асинхронных API в C# и Visual Basic. При работе без использования асинхронных API вы можете применить класс Task для выполнения длительных задач, чтобы не лишать пользователя возможности взаимодействовать с приложением. Благодаря этому ваше приложение будет реагировать на действия пользователя во время загрузки данных.
Если же приложению требуется особенно много времени на загрузку данных в определенную часть интерфейса, можно добавить в эту область строку с сообщением типа «Выполняется получение последних данных», чтобы пользователь знал, что приложение работает.
Сокращение времени запуска
Во время активации всем приложениям, кроме самых простых, требуется время на загрузку ресурсов, анализ XAML-кода, настройку структур данных и выполнение логики. Проанализируем процесс активации, разбив его на три этапа. Также мы дадим советы, как снизить время, затрачиваемое на каждом этапе, и как сделать каждый этап запуска вашего приложения более удобным для пользователя.
Период активации — это время от запуска приложения пользователем до момента, когда приложение становится полностью функциональным. Это очень важный показатель, так как он формирует у пользователей первое впечатление от вашего приложения. Они ожидают мгновенного и непрерывного отклика системы и приложений. Система и приложение воспринимаются как неисправные или плохо спроектированные, если приложения не запускаются быстро. И, что еще хуже, если приложение активируется слишком долго, оно может быть завершено диспетчером жизненного цикла процессов (PLM) или удалено пользователем.
Введение в этапы запускаЗапуск включает несколько элементов, и все они должны быть правильно скоординированы для оптимального взаимодействия с пользователем. Между моментом, когда пользователь щелкает плитку приложения, и моментом, когда отображается содержимое приложения, происходят следующие действия.
- Оболочка Windows запускает процесс, и вызывается Main.
- Создается объект приложения.
- (Шаблон проекта) Конструктор вызывает InitializeComponent, который запускает анализ App.xaml и создание объектов.
- (Шаблон проекта) Код приложения создает фрейм и осуществляет переход на MainPage.
- (Шаблон проекта) Конструктор Mainpage вызывает InitializeComponent, который запускает анализ MainPage.xaml и создание объектов.
- (Шаблон проекта) Вызывает Window.Current.Activate().
- ApplyTemplate запускает создание содержимого шаблона элемента управления для каждого элемента управления, что обычно занимает большую часть времени запуска макета.
Не включайте в код запуска ничего, что не требуется для первого фрейма.
- Если у вас есть пользовательские DLL-библиотеки, содержащие элементы управления, которые не требуются для первого фрейма, отложите их загрузку.
- Если часть пользовательского интерфейса зависит от данных в облаке, разделите этот интерфейс. Сначала вызовите интерфейс, который не зависит от данных в облаке, и асинхронно вызовите интерфейс, зависящий от облака. Также можно рассмотреть возможность помещать в кэш данные локально, так чтобы приложение работало автономно и не испытывало влияния медленного сетевого подключения.
- Если пользовательский интерфейс ожидает данные, покажите соответствующее сообщение с индикатором выполнения.
- Избегайте написания приложений с большим объемом анализа файлов конфигурации или пользовательского интерфейса, который динамически создается кодом.
Скорость запуска приложения на языке XAML непосредственно коррелирует с количеством элементов, которые создаются во время запуска. Чем меньше элементов вы создаете, тем меньше времени требуется приложению, чтобы запуститься. В первом приближении можно считать, что создание каждого элемента занимает одну миллисекунду.
- Шаблоны, используемые в элементах управления, могут оказывать максимальное влияние, так как они повторяются несколько раз. См. раздел Оптимизация пользовательского интерфейса ListView и GridView.
- UserControls и шаблоны элементов управления будут развернуты и потому должны также быть учтены.
- Если вы создаете любой элемент XAML, который не отображается на экране, вы должны обосновать необходимость их создания во время запуска.
Окно Визуальное дерево Visual Studio Live отображает количество дочерних элементов для каждого узла в дереве.
Использование отсрочки. Сворачивание элемента или установка его непрозрачности равной 0 не предотвратят создание этого элемента. С помощью атрибута x:Load или x:DeferLoadStrategy можно отложить загрузку элемента пользовательского интерфейса и загрузить его, когда это потребуется. Это хороший способ отсрочки обработки пользовательского интерфейса, который не виден во время запуска экрана, чтобы его можно было загрузить в случае необходимости или в составе набора логики с отсрочкой. Чтобы запустить загрузку, необходимо просто вызвать FindName для элемента. Примеры и дополнительные сведения см. в разделах Атрибут x:Load и Атрибут x:DeferLoadStrategy.
Виртуализация. Если у вас есть список или повторяющееся содержимое в пользовательском интерфейсе, настоятельно рекомендуется использовать виртуализацию пользовательского интерфейса. Если пользовательский интерфейс из списка не виртуализирован, выполняется предварительное создание всех элементов, и это может замедлить запуск. См. раздел Оптимизация пользовательского интерфейса ListView и GridView.
Производительность приложения — это не только объективная производительность, но также и восприятие. Изменение порядка операций, чтобы визуальные аспекты срабатывали первыми, обычно заставляет пользователя считать приложение более быстрым. Пользователи будут считать приложение загруженным, когда увидят его содержимое на экране. Часто приложениям необходимо выполнить несколько действий в рамках запуска, и не все из них требуют отображения пользовательского интерфейса, поэтому их следует откладывать или назначать им меньший приоритет, чем пользовательскому интерфейсу.
В этом разделе рассказывается о «первом фрейме», который поступает из animation/TV и является мерой скорости отображения содержимого для пользователя.
Улучшение восприятия запускаВоспользуемся простой сетевой игрой для определения каждого этапа запуска и различных методов обеспечения обратной связи с пользователем во время данного процесса. Для такой игры пример первого этапа активации — время, прошедшее от касания плитки приложения пользователем до начала выполнения кода игрой. В это время у системы нет содержимого для отображения, указывающего на корректность запуска игры. Однако предоставление экрана-заставки дает системе это содержимое. Затем игра информирует пользователя о завершении первого этапа активации, замещая статический экран-заставку собственным пользовательским интерфейсом после начала выполнения кода.
Второй этап активации охватывает создание и инициализацию критических для игры структур. Если приложение способно быстро создать свой исходный пользовательский интерфейс с данными, которые становятся доступными после первого этапа активации, то второй этап вполне обычный, и пользовательский интерфейс может отображаться немедленно. В противном случае рекомендуется, чтобы во время инициализации приложение отображало страницу загрузки.
Вид такой страницы загрузки зависит от вас. На ней может просто отображаться индикатор выполнения в виде полоски или кольца. Здесь главное заключается в том, что приложение указывает на необходимость выполнения задач, прежде чем начнет реагировать на запросы. В случае рассматриваемой игры хотелось бы, чтобы отображался исходный экран, однако пользовательскому интерфейсу требуется загрузить с диска в память некоторые изображения и звуки. Такие задачи занимают пару секунд, поэтому приложение показывает пользователю, что не зависло, замещая экран-заставку страницей загрузки с простой анимацией на тему игры.
Третий этап начинается, когда у приложения есть минимальный набор информации для создания интерактивного пользовательского интерфейса, замещающего страницу загрузки. В этот момент единственная информация, доступная сетевой игре, — содержимое, загруженное приложением с диска. Игра может поставляться с содержимым, достаточным для создания интерактивного пользовательского интерфейса. Однако, так как это сетевая игра, она не будет функционировать без подключения к Интернету и скачивания некоторой дополнительной информации. Пока не будет получена вся требуемая для функционирования информация, пользователь может взаимодействовать с пользовательским интерфейсом, но компоненты, которым нужны дополнительные данные из сети, должны обеспечить определенную обратную связь, пока загружается содержимое. Переход приложения в полностью функциональное состояние может занять некоторое время, поэтому важно, чтобы функциональные возможности стали доступны как можно скорее.
Теперь, после определения трех этапов активации сетевой игры, свяжем их с фактическим кодом.
Этап 1Прежде чем приложение запустится, оно должно сообщить системе, что требуется отображать на экране-заставке. Для этого в элементе SplashScreen в манифесте приложения указывается изображение и цвет фона, как в данном примере. Windows отображает их после начала активации приложения.
Дополнительные сведения об этом см. в статье Добавление экрана-заставки.
Используйте конструктор приложения только для инициализации критичных для него структур данных. Конструктор вызывается только при первом запуске приложения. При последующих активациях приложения в этом нет необходимости. Например, конструктор не вызывается для приложений, которые запущены, переведены в фоновый режим, а затем активированы через контракт «Поиск».
Этап 2Есть несколько причин активации приложения, каждую из которых можно обрабатывать по-разному. Для обработки каждой причины активации можно переопределить методы OnActivated, OnCachedFileUpdaterActivated, OnFileActivated, OnFileOpenPickerActivated, OnFileSavePickerActivated, OnLaunched, OnSearchActivated и OnShareTargetActivated. Помимо всего прочего приложение с помощью этих методов должно создать пользовательский интерфейс, назначить его свойству Window.Content, а затем вызвать метод Window.Activate. В этот момент экран-заставка замещается пользовательским интерфейсом, созданным приложением. Этот визуальный элемент может быть экраном загрузки или фактическим пользовательским интерфейсом приложения, если во время активации достаточно информации для его создания.
Приложения, отображающие страницу загрузки в обработчике активации, начинают работать над созданием пользовательского интерфейса в фоновом режиме. После создания такого элемента возникает соответствующее событие FrameworkElement.Loaded. В обработчике событий содержимое окна (в данный момент экрана загрузки) замещается недавно созданной домашней страницей.
Важно, чтобы приложение с длительной инициализацией отображало страницу загрузки. Кроме обеспечения обратной связи во время активации следует знать, что процесс приложения будет завершен, если метод Window.Activate не вызывается в течение 15 секунд после начала активации.
Пример расширенных экранов-заставок см. в разделе Образец экрана-заставки.
Этап 3Если приложение отобразило пользовательский интерфейс, это еще не означает, что оно полностью готово к использованию. В случае нашей игры пользовательский интерфейс отображается с заполнителями для компонентов, которым требуются данные из Интернета. В этот момент игра скачивает дополнительные данные, необходимые приложению для полного функционирования. Компоненты постепенно становятся доступными по мере получения данных.
Иногда значительную часть содержимого, необходимого для активации приложения, можно упаковать с приложением. Это происходит в случае простой игры. Процесс активации становится совсем простым. Однако многие программы (например, программы для чтения новостей, просмотра фотографий), чтобы стать функциональными, должны получать информацию из сети. Объем таких данных может быть большим, и скачивание может занять довольно много времени. Способ получения приложением данных во время активации может оказать огромное влияние на восприятие производительности приложения.
Страница скачивания или, еще хуже, экран-заставка может отображаться несколько минут, если приложение пытается скачать весь набор данных, необходимых для функционирования на первом или втором этапе активации. В результате приложение выглядит зависшим, или его работа завершается системой. Мы рекомендуем, чтобы на этапе 2 приложение скачивало минимальный объем данных для отображения интерактивного пользовательского интерфейса с элементами-заполнителями, а затем на этапе 3 постепенно подкачивало данные для замены таких элементов. Подробнее о работе с данными см. в статье Оптимизация ListView и GridView.
Насколько точно приложение реагирует на каждый этап запуска, полностью зависит от вас, однако если обеспечивать максимально возможную обратную связь с пользователем (экран-заставка, экран загрузки, пользовательский интерфейс во время загрузки данных), то в целом пользователь будет считать приложение и систему быстрыми.
Минимизация управляемых сборок в пути запускаПовторное использование кода часто реализуется в виде включения в проект модулей (DLL-файлов). Для загрузки таких модулей требуется доступ к диску и, как легко можно представить, затраты ресурсов для этого могут возрасти. Это прежде всего влияет на холодный запуск, но также может повлиять и на горячий запуск. В случае языков C# и Visual Basic среда CLR пытается максимально снизить эти затраты, загружая сборки по требованию. То есть среда CLR не загружает модуль, пока на него не ссылается выполняемый метод. Поэтому в коде запуска следует ссылаться только на сборки, необходимые для запуска приложения, чтобы среда CLR не загружала лишние модули. Если в пути запуска есть неиспользуемые ветви кода, в которых присутствуют ненужные ссылки, то такие ветви кода можно переместить в другие методы, чтобы избежать ненужных загрузок.
Еще один способ сокращения загрузок модулей — объединить модули приложения. Обычно одна большая сборка загружается быстрее двух сборок меньшего размера. Но это не всегда возможно, к тому же объединение модулей оправданно, только когда оно не оказывает заметного влияния на производительность разработчика или повторное использование кода. Загружаемые при запуске модули выявляются с помощью таких инструментов, как PerfView или Windows Performance Analyzer (WPA).
Создание интеллектуальных веб-запросовВремя загрузки приложения можно существенно сократить, локально упаковав его содержимое в приложение, включая XAML-код, изображения и любые другие файлы, необходимые для работы. Операции с диском быстрее сетевых операций. Если во время инициализации приложению нужен конкретный файл, общее время загрузки можно сократить, если загружать его с диска, а не с удаленного сервера.
Страницы журнала и кэша
Элемент управления Frame предоставляет функции навигации. Он обеспечивает переход на страницу (метод Navigate), ведение журнала навигации (свойства BackStack/ForwardStack, метод GoForward/GoBack), кэширование страницы (Page.NavigationCacheMode) и поддержку сериализации (метод GetNavigationState).
Производительность фрейма, которую следует учитывать, в основном связана с ведением журнала и кэшированием страниц.
Ведение журнала фреймов. При переходе на страницу с Frame.Navigate() PageStackEntry для текущей страницы добавляется в коллекцию Frame.BackStack. Элемент PageStackEntry относительно мал, но никаких встроенных лимитов на размер коллекции BackStack не существует. Теоретически пользователь может осуществлять циклическую навигацию и расширять эту коллекцию бесконечно.
PageStackEntry также содержит параметр, переданный методу Frame.Navigate(). Рекомендуется, чтобы этот параметр имел примитивный сериализуемый тип (например, целое число или строка), чтобы обеспечить работу метода Frame.GetNavigationState(). Однако этот параметр потенциально может сослаться на объект, который отвечает за более значительные количества рабочего набора или других ресурсов, что сделает каждую запись в BackStack намного более ресурсоемкой. Например, теоретически можно использовать StorageFile как параметр, и поэтому BackStack держит открытыми неопределенное количество файлов.
Поэтому рекомендуется сохранить небольшие значения параметров навигации и ограничить размер BackStack. BackStack — это стандартный вектор (IList на C#, Platform::Vector на C++/CX), поэтому может быть просто обрезан путем удаления записей.
Кэширование страницы. По умолчанию при переходе на страницу с помощью метода Frame.Navigate создается новый экземпляр страницы. Аналогично, если затем вернуться на предыдущую страницу с помощью метода Frame.GoBack, будет выделен новый экземпляр предыдущей страницы.
Однако фрейм предлагает дополнительный кэш страницы, который позволяет избежать создания этих экземпляров. Чтобы поместить страницу в кэш, используйте свойство Page.NavigationCacheMode. Если сделать этот режим обязательным («Обязательно»), страницы будут кэшироваться принудительно; если же выбрать параметр «Включен», будет разрешено кэширование. По умолчанию размер кэша составляет 10 страниц, но это значение можно переопределить с помощью свойства Frame.CacheSize. Все обязательные страницы будут помещены в кэш, и если их окажется меньше, чем CacheSize, страницы с включенным параметром кэширования также можно будет поместить в кэш.
Кэширование страниц может повысить производительность за счет исключения необходимости создания лишних экземпляров, а значит, увеличится скорость навигации. Кэширование страниц может снизить производительность в случае избыточного кэширования, которое отрицательно влияет на рабочий набор.
Поэтому рекомендуется использовать соответствующее кэширование страниц для приложения. Предположим, у вас есть приложение, которое отображает список элементов во фрейме, и при касании элемента он переводит фрейм на страницу сведений для этого элемента. Страница списка, вероятно, должна быть помещена в кэш. Если страница сведений одинакова для всех элементов, ее также следует поместить в кэш. Но если страница сведений неоднородна, возможно, удобнее будет отключить кэширование.