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

Фазы пайплайна рендеринга

  1. Сборка (Build): создание и настройка виджетов на основе текущего состояния приложения.
  1. Макет (Layout): определение размера и положения каждого виджета (родительские ограничения определяют размеры дочерних).
  1. Отрисовка (Paint): отрисовка на canvas визуальных элементов (например, цвета, текста, рамок).
  1. Компоновка (Compositing): организация отрисованных виджетов по слоям.
  1. Растеризация (Rasterize): преобразование отрисованных слоев в пиксели, удобные для распознавания GPU, для последующего отображения.

Если вы откроете DevTools таким образом, то в середине увидите результат фазы сборки, а справа — результат фазы макета.

Flutter использует цикл событий для планирования обратных вызовов фреймов. Определив, что требуется новый фрейм — из-за взаимодействия с пользователем, анимации или изменения состояния, — Flutter вызывает методы вроде scheduleFrame на WidgetsBinding. Это запланирует обратный вызов, который будет добавлен в цикл событий Dart.

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

Если необходимо выполнить какую-то логику сразу после пайплайна рендеринга, можно использовать постфреймовые обратные вызовы. Наиболее распространенный случай — запуск контекстно-зависимой логики в методе initState:

@override
void initState() {
super.initState();
context; // <- здесь произойдет сбой, виджет еще не прикреплен
}

@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context; // <- работает
});
}

Что происходит в фазе сборки

Во время фазы сборки работа выполняется в основном в Widgets (слое виджетов) и Elements (слое элементов), а управляется в основном BuildOwner.

  • Widgets:

Flutter вызывает метод build() каждого виджета, чтобы создать новое дерево виджетов, отражающее текущее состояние и конфигурацию UI. Когда выполняется build(), состояние блокируется, то есть вызов setState() запрещен.

  • Elements:

Слой Element выступает в качестве посредника между неизменяемым слоем виджетов и изменяемыми объектами рендеринга. Во время фазы сборки Flutter использует элементы для согласования нового дерева виджетов (исходников?) с предыдущим, гарантируя обновление только необходимых частей UI и освобождая следующие фазы рендеринга от лишней работы, что делает сложность рендеринга сублинейной.

Хотя объекты рендеринга имеют решающее значение для макета и отрисовки UI, они создаются и обновляются в последующих фазах (макета и отрисовки), а не непосредственно во время фазы сборки.

Сублинейная сложность

Когда говорится, что рендеринг во Flutter является сублинейным, имеется в виду, что вычислительные усилия, необходимые для рендеринга UI, растут медленнее, чем линейно, с увеличением количества виджетов или элементов. Другими словами, если удваивается количество виджетов, затраты времени или работы, необходимые для рендеринга фрейма, не обязательно удваиваются.

Как Flutter добивается этого?

Telegram-канал по теме
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться

Сравнение деревьев виджетов

Когда состояние меняется, Flutter запускает процесс сравнения нового дерева виджетов с предыдущим (Widget Tree Diffing) и обновляет только те части, которые изменились. Однако ваша задача — уведомить об изменении состояния и обеспечить оптимальную область обновления виджета, вызвав setState в правильном контексте (или корректно используя любое другое управление состоянием). Обновление области видимости принимает в качестве аргумента Element, который как раз и инкапсулирует BuildContext.

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

Рендеринг на основе слоев

Flutter рендерит UI-элементы по слоям, которые затем компонуются GPU. Если меняется лишь небольшая часть UI, то обновлять нужно только соответствующий(е) слой(и). Ваша задача состоит в том, чтобы оптимально изолировать различные части UI по компонуемым слоям.

Кэширование и ленивые обновления

Многие виджеты неизменяемы и могут кэшироваться. Если виджет или его поддерево не изменились, Flutter повторно использует существующую информацию о рендеринге. Такая стратегия кэширования позволяет избежать избыточных вычислений и операций отрисовки. Именно в этот момент использование виджетов const выполняет свою задачу.

4.  Виджеты

Теперь разберем, что происходит в слое виджетов.

Вызов метода build

Когда виджету необходимо обновиться (из-за изменения состояния, изменения родителя или других триггеров), вызывается его метод build(). Этот метод возвращает новую конфигурацию виджетов, которая описывает UI в данный момент.

Создание дерева неизменяемых виджетов

Виджеты во Flutter неизменяемы. Новое дерево виджетов, созданное методом build(), служит образцом того, как должен выглядеть UI. Это дерево является легковесным и содержит только данные конфигурации, а не реальные UI-элементы.

Использование ключей

Виджеты могут использовать ключи для сохранения своей идентичности при перестройке. Это особенно важно для списков или динамических интерфейсов, поскольку ключи помогают Flutter сопоставлять новые экземпляры виджетов с соответствующими элементами в существующем дереве.

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

Элементы

Понимать, что происходит в слое Element, необходимо для того, чтобы сохранить дерево Element оптимальным и избежать избыточного выделения.

  • В этой фазе рендеринга элементы создаются, монтируются и обновляются. Фреймворк вызывает mount, чтобы добавить только что созданный элемент в дерево Element. Эта операция называется BuildContext.mounted. Если Element не был добавлен в дерево Element (или удален из него), BuildContext будет недействительным для использования.
  • Element после того, как будет размонтирован, не может быть смонтирован обратно в дерево. Это односторонний переход. Element считается «недействующим» («defunct») и не будет включен в дерево в будущем.
  • Хотя сам размонтированный Element не может быть использован повторно, если у него был GlobalKey, этот ключ может быть повторно использован в новом Element. Этот новый Element будет полностью отделен от размонтированного.
  • Чтобы запланировать (пере)сборку, Flutter помечает обновленные элементы как грязные, а затем в фазе сборки итерирует по ним и вызывает их метод performRebuild(). Этот метод, в свою очередь, вызывает метод build() элемента, чтобы сгенерировать новое поддерево виджетов, которое, в свою очередь, вызывает метод build() виджета и передает себя в качестве BuildContext.

Элементы могут размонтироваться в нескольких сценариях. Рассмотрим основные сценарии размонтирования элементов и затем сравним их со сценариями деактивирования элементов.

Завершение очистки фрейма

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

Widget build(BuildContext context) {
return isVisible
? MyWidget() // Когда isVisible становится false:
// 1. MyWidget's Element деактивируется
// 2. Позже размонтируется в конце фрейма
: SizedBox(); // Создается новый Element
}

Навигация

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

Navigator.pop(context);

Теперь посмотрим, когда элемент деактивируется, а затем снова активируется.

Виджет Offstage

Элемент остается активным даже при скрытии, в отличие от условного рендеринга, который деактивирует его.

Offstage(
offstage: isHidden,
child: MyWidget(),
);

Перемещение виджета между родителями с помощью GlobalKey

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

Переупорядочивание элементов списка

Когда вы переупорядочиваете элементы в ListView, алгоритм согласования Flutter с помощью уникальных ключей, назначенных каждому виджету, повторно использует связанные с ними Elements.

В фазе сборки Flutter сравнивает новый список виджетов с предыдущим, проверяя ключ каждого виджета:

  • если виджет с таким же ключом существует в предыдущем списке, Flutter повторно использует его Element;
  • если необходимо, существующий Element обновляется с новой конфигурацией виджета, но его состояние и другие связанные с ним данные остаются нетронутыми.

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

AnimatedSwitcher с тем же дочерним типом

        AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: showFirst
? Container(
key: childKey, // Element деактивируется во время анимации
color: Colors.blue,
height: 100,
width: 100,
)
: Container(
key: childKey, // и повторно активируется в новом положении
color: Colors.red,
height: 100,
width: 100,
),
),

Сравнение деактивированных и размонтированных элементов

Различия:

  • Деактивированные Elements сохраняют отношения унаследованных виджетов (InheritedWidgets).
  • Деактивация временная (длится до конца фрейма).
  • Размонтированные Elements очищают все отношения.
  • Размонтирование является постоянным.

Читайте также:


Перевод статьи Roman Ismagilov: Understanding Flutter rendering pipeline: Build phase

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяC++: полное руководство по операторам Switch со строками
Следующая статьяКак стать ценным программистом