Понятие конвейера рендеринга Flutter: фаза сборки
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Конвейер рендеринга Flutter — это высокооптимизированный процесс, который преобразует ваш декларативный код интерфейса в пиксели на экране. С глубоким пониманием конвейера рендеринга вы можете принимать более обоснованные решения о структуре виджетов и управлении состоянием, что приводит к более поддерживаемому и масштабируемому коду.
Конвейер рендеринга Flutter — это высокооптимизированный процесс, который преобразует ваш декларативный код интерфейса в пиксели на экране. С глубоким пониманием конвейера рендеринга вы можете принимать более обоснованные решения о структуре виджетов и управлении состоянием, что приводит к более поддерживаемому и масштабируемому коду.
1. Фазы конвейера рендеринга
- Построение: Виджеты создаются и настраиваются на основе текущего состояния приложения.
- Разметка: Определяет размер и положение каждого виджета (ограничения родителя направляют размеры дочерних элементов).
- Отрисовка: Рисует визуальные элементы (например, цвета, текст, границы) на холсте.
- Композитинг: Организует нарисованные виджеты в слои
- Растеризация: Преобразует эти нарисованные слои в пиксели, дружелюбные для GPU, для окончательного отображения.

Если вы откроете DevTools, в центре вы увидите результат фазы построения, а справа — результат фазы разметки.
Flutter использует цикл событий для планирования обратных вызовов кадров. Когда Flutter определяет, что нужен новый кадр — из-за взаимодействий пользователя, анимаций или изменений состояния — он вызывает методы, такие как scheduleFrame в WidgetsBinding. Это планирует обратный вызов, который добавляется в цикл событий Dart.
Как только обратный вызов кадра выполняется, Flutter проходит через свой конвейер рендеринга. Каждая фаза может запланировать дополнительную работу (например, микро задачи или дополнительные обратные вызовы кадров), и все они координируются через цикл событий.
Если вы хотите выполнить какую-то логику сразу после конвейера рендеринга, вы можете использовать post frame callbacks. Наиболее распространенный случай — запуск какой-то контекстно-зависимой логики в методе initState:
@override
void initState() {
super.initState();
context; // <- здесь не работает, виджет еще не прикреплен
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context; // <- работает
});
}
2. Что происходит во время фазы построения
Во время фазы построения работа выполняется в основном в Widgets и Elements, в основном управляемая BuildOwner.
- Виджеты:
Flutter вызывает методbuild()каждого виджета для создания нового дерева виджетов, отражающего текущее состояние и конфигурацию вашего интерфейса. Когда выполняетсяbuild(), состояние заблокировано, то есть вызовsetState()не разрешен. - Слой элементов:
СлойElementвыступает посредником между неизменяемым слоем виджетов и изменяемыми объектами рендеринга. Во время фазы построения Flutter использует элементы для согласования нового дерева виджетов (источника?) с предыдущим, обеспечивая обновление только необходимых частей интерфейса, чтобы последующие фазы рендеринга не выполняли избыточную работу, что делает сложность рендеринга сублинейной.
Хотя объекты рендеринга являются критически важными для фактической разметки и отрисовки вашего интерфейса, они создаются и обновляются в последующих фазах (разметка и отрисовка), а не непосредственно во время фазы построения.
3. Сублинейная сложность
Когда мы говорим, что рендеринг Flutter сублинейный, мы имеем в виду, что вычислительные усилия, необходимые для рендеринга интерфейса, растут медленнее, чем линейно с количеством виджетов или элементов. Другими словами, если вы удвоите количество виджетов, время или работа, необходимые для рендеринга кадра, не обязательно удвоятся.
Как Flutter этого достигает?
Сравнение дерева виджетов
Когда состояние изменяется, Flutter сравнивает новое дерево виджетов с предыдущим и обновляет только те части, которые изменились. Однако это наша задача — уведомлять об изменении состояния и предоставлять оптимальную область обновления виджета область обновления путем вызова setState в правильном контексте (или правильного использования любого другого управления состоянием). Обновление области принимает Element в качестве аргумента, что именно и BuildContextинкапсулирует.
Даже если сравнение виджетов было эффективным и не было бы много избыточного рендеринга, выполнение всех методов построения все равно требует некоторой вычислительной мощности.
Рендеринг на основе слоев
Flutter отображает элементы интерфейса в слои, которые затем композируются GPU. Если изменяется только небольшая часть интерфейса, обновляются только соответствующие слои. Наша задача здесь — изолировать различные части интерфейса в композитные слои оптимальным образом.
Кэширование и ленивые обновления
Многие виджеты неизменяемы и могут быть закэшированы. Если виджет или его поддерево не изменились, Flutter использует существующую информацию для рендеринга. Эта стратегия кэширования исключает избыточные вычисления и операции отрисовки. Вот когда использование виджетов const выполняет свою задачу.
4. Виджеты
Разберём, что происходит на уровне виджетов:
Вызов метода build:
Когда виджет нужно обновить (из-за изменений состояния, изменений родителя или других триггеров), вызывается его метод build(). Этот метод возвращает новую конфигурацию виджетов, описывающую интерфейс в данный момент.
Создание неизменяемого дерева виджетов:
Виджеты в Flutter неизменяемы. Новое дерево виджетов, созданное методом build(), служит чертежом того, как должен выглядеть интерфейс. Это дерево лёгковесное и содержит только данные конфигурации, а не сами элементы интерфейса.
Использование ключей:
Виджеты могут использовать ключи для сохранения своей идентичности при пересборке. Это особенно важно в списках или динамических интерфейсах, так как ключи помогают Flutter сопоставлять новые экземпляры виджетов с соответствующими элементами в существующем дереве.
После построения нового дерева виджетов Flutter передаёт его на уровень элементов. Если вы хотите узнать больше о том, как Flutter преобразует виджеты в элементы, вот документация.
5. Элементы
Важно понять, что происходит на уровне Element, чтобы поддерживать дерево Element в оптимальном состоянии и избегать избыточных выделений памяти.
Elementsсоздаются, монтируются и обновляются на этом этапе. Фреймворк вызываетmountдля добавления только что созданного элемента в деревоElement. Вот что означаетBuildContext.mounted. ЕслиElementне был добавлен (или удалён) из дереваElement, тоBuildContextнедействителен для использования.- Как только
Elementдемонтируется, его нельзя снова смонтировать в дерево. Это односторонний переход.Elementсчитается "недействительным" и в будущем не будет включён в дерево. - Хотя демонтированный
Elementсам по себе не может быть использован повторно, если у него былGlobalKey, этот ключ можно использовать с новымElement. НовыйElementбудет полностью отдельным от демонтированного. - Чтобы запланировать (пере)сборку, Flutter помечает обновлённые Elements как грязные, а затем во время фазы сборки Flutter перебирает их и вызывает их метод
performRebuild(). Этот метод, в свою очередь, вызывает методbuild()элемента для генерации нового поддерева виджетов, которое затем вызывает методbuild()виджетов и передаёт себя какBuildContext.
Elements демонтируются в нескольких сценариях. Вот основные случаи, когда Elements демонтируются, а затем мы сравним их со сценариями, когда Elements деактивируются.
Очистка в конце кадра
В конце кадра процесс очистки Flutter перебирает дерево элементов, находит элементы, которые больше не прикреплены (т.е. сироты), и вызывает их метод unmount() для их отсоединения и освобождения связанных ресурсов. Это может произойти из-за удаления родителя или использования условного виджета.
Widget build(BuildContext context) {
return isVisible
? MyWidget() // Когда isVisible становится false:
// 1. Element MyWidget деактивируется
// 2. Позже демонтируется в конце кадра
: SizedBox(); // Создаётся новый Element
}
Навигация
Когда маршрут полностью удаляется, всё дерево Element этого маршрута размонтируется после завершения анимации
Navigator.pop(context);
Теперь давайте посмотрим, когда Element деактивируется, а затем снова активируется:
Виджет Offstage
Element остаётся активным даже когда скрыт, в отличие от условного рендеринга, который бы его деактивировал.
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сохраняют свои наследуемые виджеты отношения - Деактивация временная (продолжается до конца кадра)
- Размонтированные
Elementsочищают все отношения - Размонтирование постоянное
