3.4. Сервисы связи
Когда мы рассматривали архитектуру Flutter-приложения, — основные слои и их компоненты, — то вскользь упомянули про Foundation. Продублируем изображение, чтобы вы были в контексте.
Мы выяснили, что Foundation — это низкоуровневые классы-утилиты, которые используются другими слоями фреймворка, а ещё тут происходит самое важное — связывание приложения со слоем Engine.
За связывание как раз отвечают сервисы связи (англ. Bindings). В этой статье мы разберём, что это такое, какие сервисы связи бывают, за что они отвечают и как связаны с рендерингом.
Определение сервисов связи
Начнём чуть издалека. Вы помните, что запуск каждого Flutter-приложения начинается с вызова функции runApp.
Давайте посмотрим на её реализацию.
void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
assert(binding.debugCheckZone('runApp'));
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
В функции происходят три важных действия:
- Получается и инициализируется, если необходимо, объект
bindingс типомWidgetsBinding. - Вызывается метод
scheduleAttachRootWidget, в который передаётся переданный в функцию виджет. - Откладывается операция отрисовки первого кадра —
scheduleWarmUpFrame.
Давайте сфокусируемся на первом действии и разберёмся, какую роль оно играет в запуске нашего приложения.
Сервисы связи (англ. Bindings) — это некоторые связующие классы, выполняющие роль «клея» между движком и фреймворком. А если говорить формальным языком — интерфейсы обмена данными между Flutter framework и Flutter engine.
Вот уже знакомая вам схема — для наглядности и контекста.
По правде говоря, это не чистая связь между кодом движка на C++ и Dart-кодом, а связь между PlatformDispatcher из dart:ui и более высокоуровневыми слоями фреймворка, своего рода набор фасадов над PlatformDispatcher.
Какие бывают сервисы связи
Базовый класс для всех сервисов связи — BindingBase. Конкретные сервисы наследуются именно от него.
Они обязуются гарантировать единственность своего экземпляра и инициализируются только один раз (реализуют паттерн «Синглтон»). Каждый сервис отделяет в себе обработку ограниченного набора задач, связанных непосредственно с ним. В сервисе GestureBinding, например, обрабатываются задачи, связанные со взаимодействием пользователя с экраном устройства.
Всего во Flutter девять наследников класса BindingBase:
SchedulerBinding;ServicesBinding;GestureBinding;RendererBinding;SemanticsBinding;PaintingBinding;WidgetsBinding;WidgetsFlutterBinding;TestWidgetsFlutterBinding.
Bindings связаны между собой определённой структурой: одни сервисы связи также являются миксинами над другими, более низкоуровневыми. Зависимости между ними можно представить следующей схемой:
Давайте взглянем на каждый сервис связи и его задачи подробнее.
SchedulerBinding
Главная задача этого сервиса — планировка задач, связанных с отрисовкой кадра. Например:
- Вызовы преходящих задач (
transientCallbacks), которые инициирует система в методеWindow.onBeginFrame. Например, события тикеров и контроллеров анимации. - Не связанные с рендерингом задачи (
midFrameMicrotasks), которые должны быть выполнены между кадрами. То есть микротаски, запланированные преходящими задачами. Это может быть очистка очереди событий обработанных жестов, обработка скролла. Микротаски выполняются между подготовкой к новому кадру и его отрисовкой. - Вызовы непрерывных задач (
persistentCallbacks), которые инициирует система в методеWindow.onDrawFrame. В частности, это вызов методаbuildу виджета илиlayoutу рендер-объекта. - Задачи, вызываемые после отрисовки кадра (
postFrameCallbacks). Обычно это задачи, которые не могут выполниться в процессе рендеринга, например отправка семантических событий (изменение фокуса) или очистка кеша изображений.
Одновременно SchedulerBinding может работать только с одним типом задач, обработка идёт в том порядке, в котором мы перечислили их выше.
Узнать, какую задачу в данный момент обрабатывает SchedulerBinding, можно с помощью геттера schedulerPhase, который возвращает состояние SchedulerPhase.
Далее поговорим о том, где будет полезен SchedulerBinding и его методы.
addPostFrameCallback и endOfFrame
Очень часто нам нужно выполнить код после окончания рендеринга текущего кадра, когда станут доступны размеры всех виджетов: например, чтобы получить размер виджета, который не известен заранее.
Для этого мы можем воспользоваться двумя способами — методом addPostFrameCallback или геттером endOfFrame.
Пример
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: TextGeometryExample(),
);
}
}
class TextGeometryExample extends StatefulWidget {
const TextGeometryExample({super.key});
@override
State<TextGeometryExample> createState() => _TextGeometryExampleState();
}
class _TextGeometryExampleState extends State<TextGeometryExample> {
final GlobalKey textKey = GlobalKey();
Size size = const Size(0, 0);
@override
void initState() {
super.initState();
/// Добавляем [postFrameCallback] в [SchedulerBinding]
/// Он вызовется после отрисовки текущего кадра, в частности после
/// отрисовки текста с [GlobalKey] = [textKey].
///
/// Размеры виджета text будут известны
/// в момент исполнения [_changeAnimatedContainerDimensions],
/// тк он вызывается ПОСЛЕ отрисовки кадра, т.е. размер текста будет посчитан
SchedulerBinding.instance
.addPostFrameCallback(_changeAnimatedContainerDimensions);
/// Аналогичного результата можно достичь используя Future api
/// и геттер [endOfFrame] у [SchedulerBinding].
///
/// Future будет завершен тогда, когда завершится отрисовка текущего кадра
///
/// Пример:
/// SchedulerBinding.instance.endOfFrame
/// .then((_) => _changeAnimatedContainerDimensions());
}
void _changeAnimatedContainerDimensions(
[Duration? postframeCallbackDuration]) {
/// Получаем [RenderObject] который относится к виджету Text.
/// После отрисовки в нем есть информация о геометрии виджета.
RenderBox logoBox = textKey.currentContext!.findRenderObject() as RenderBox;
/// Получаем размер
size = Size(
logoBox.size.width + 5,
logoBox.size.height + 5,
);
/// Обновляем стейт. Изменения размера [size] спровоцирует анимацию у [AnimatedContainer]
setState(() {});
}
@override
Widget build(BuildContext context) {
/// [Stack] используется намеренно, чтобы продемонстрировать возможность
/// получить размер виджета, расположенного параллельно в дереве.
///
/// Данного эффекта можно добиться и не используя [postFrameCallback]
return Scaffold(
body: Stack(
children: [
/// Контейнер с конкретным размером. Ожидается что в переменной size
/// будет размер текста, расположенного ниже по стеку.
///
/// Текст в виджете ниже может быть произвольный и на момент верстки
/// мы не можем точно знать его размер.
Center(
child: AnimatedContainer(
duration: const Duration(seconds: 2),
curve: Curves.bounceInOut,
width: size.width,
height: size.height,
color: Colors.amber,
),
),
Center(
child: Text(
key: textKey,
'Динамический текст',
style: const TextStyle(fontSize: 20),
),
),
],
),
);
}
}
Ещё один вариант использования SchedulerBinding — возможность наблюдать за метриками отрисовки вашего приложения.
Наблюдение за производительностью
С помощью SchedulerBinding вы можете наблюдать за производительностью вашего приложения, используя следующие методы:
// [FrameTiming] — объект с информацией о кадре
typedef TimingsCallback = void Function(List<FrameTiming> timings);
// Добавить [TimingsCallback]
void addTimingsCallback(TimingsCallback callback)
// Удалить [TimingsCallback]
void removeTimingsCallback(TimingsCallback callback)
Здесь FrameTiming — объект, который содержит метрики отрисовки кадра, такие как:
- длительность фазы build
- длительность фазы отрисовки на GPU
Эти метрики собираются в батчи и отправляются примерно раз в секунду в release-режиме сборки. Разработчики Flutter утверждают, что за каждый зарегистрированный TimingsCallback использование процессора вырастет примерно на 0,01%, — это замедляет перформанс приложения, и нужно пользоваться этим функционалом с осторожностью.
Батч (англ. batch) — набор данных, собранный в группу. Это позволяет не отправлять данные часто и экономить ресурсы при их частой отправке.
Пример использования TimingsCallback:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
/// Виджет приложения
class MyApp extends StatelessWidget {
const MyApp([Key? key]) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: PerformanceMeasurePage(title: 'Performance measure'),
),
);
}
}
/// Страница со сбором метрики рендеринга
class PerformanceMeasurePage extends StatefulWidget {
const PerformanceMeasurePage({super.key, required this.title});
final String title;
@override
State<PerformanceMeasurePage> createState() => _PerformanceMeasurePageState();
}
class _PerformanceMeasurePageState extends State<PerformanceMeasurePage> {
/// Показывать или нет [CircularProgressIndicator]
/// [CircularProgressIndicator] это постоянная анимация
/// Здесь он используется чтобы намеренно создать нагрузку на рендеринг
bool showProgressIndicator = false;
/// [StreamController] для событий TimingsCallback.
late final StreamController<FrameTiming> _frameTimingsStreamController;
@override
void initState() {
super.initState();
/// Инициализация [StreamController]
_frameTimingsStreamController = StreamController<FrameTiming>.broadcast();
/// Регистрируем [TimingsCallback] в [SchedulerBinding]
/// Лучше не использовать анонимную функцию, тк есть риск потерять на нее ссылку и не удалить обработчик
/// Поэтому используется внутренний метод, ссылка на который для объекта состояния не поменяется
SchedulerBinding.instance.addTimingsCallback(_onTimingsCallback);
}
@override
void dispose() {
/// Удаляем [TimingsCallback] чтобы не создавать дополнительную нагрузку на CPU
SchedulerBinding.instance.removeTimingsCallback(_onTimingsCallback);
_frameTimingsStreamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Spacer(flex: 3),
NonAnimatedButton(
onTap: _onCallSetstateButtonTapped,
text: 'Вызвать setState',
),
const SizedBox(height: 20),
NonAnimatedButton(
onTap: _onShowAnimationButtonTapped,
text: 'Показать CircularProgressIndicator',
),
const SizedBox(height: 20),
if (showProgressIndicator) const CircularProgressIndicator(),
const Spacer(),
AverageFrameStats(
frameTiming: _frameTimingsStreamController.stream,
),
const Spacer(flex: 3)
],
),
),
);
}
void _onCallSetstateButtonTapped() {
setState(() {});
}
void _onShowAnimationButtonTapped() {
setState(() {
showProgressIndicator = !showProgressIndicator;
});
}
void _onTimingsCallback(List<FrameTiming> timings) {
for (final timing in timings) {
// Добавление в streamController через add для примера
// В реальном приложении лучше предусмотреть механизм тротлинга
// элементов [FrameTiming] перед вызовом метода add, если есть необходимость использовать Stream Api
// Может вызывать джанки т.к. потенциально создает много микро-задач (microtasks)
_frameTimingsStreamController.add(timing);
}
_reportTimings(timings);
}
void _reportTimings(List<FrameTiming> timings) {
// Можно отправлять аналитику про длительность кадров
// Например, используя AppMetrica
//
// Обычно требуется какая-то минимальная пред-обработка:
// Отправлять огромное количество метрики про каждый кадр не рационально,
// Лучше собрать какую-то статистику сессии на устройстве и отправить ее
}
}
/// Кнопка без анимаций.
/// Стандартные кнопки из библиотеки Material используют [InkWell]
/// Это создает лишнюю нагрузку на рендеринг, а мы хотим посмотреть как
/// работает [TimingsCallback] без лишнего шума
class NonAnimatedButton extends StatelessWidget {
final VoidCallback onTap;
final String text;
const NonAnimatedButton({
required this.onTap,
required this.text,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: DecoratedBox(
decoration: const BoxDecoration(
color: Colors.lightBlue,
),
child: SizedBox(
width: 130,
height: 56,
child: Center(child: Text(text)),
),
),
);
}
}
/// Виджет со сбором статистики про фреймы.
/// Получает на вход поток данных [FrameTiming] и агрегирует их в статистику
class AverageFrameStats extends StatefulWidget {
final Stream<FrameTiming> frameTiming;
const AverageFrameStats({required this.frameTiming, Key? key})
: super(key: key);
@override
State<AverageFrameStats> createState() => _AverageFrameStatsState();
}
class _AverageFrameStatsState extends State<AverageFrameStats> {
StreamSubscription<FrameTiming>? _framesStreamSub;
int currentFrame = 0;
int maxBuildDurationMs = 0;
int maxRasterDurationMs = 0;
@override
void initState() {
_framesStreamSub = widget.frameTiming.listen(_onFrameEvent);
super.initState();
}
@override
void didUpdateWidget(covariant AverageFrameStats oldWidget) {
if (oldWidget.frameTiming != widget.frameTiming) {
_framesStreamSub?.cancel();
_framesStreamSub = widget.frameTiming.listen(_onFrameEvent);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_framesStreamSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
width: 2,
),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Text('Номер текущего кадра $currentFrame'),
const SizedBox(height: 10),
Text(
'Макс. продолжительность сборки кадра в UI $maxBuildDurationMs мс',
),
const SizedBox(height: 10),
Text(
'Макс. продолжительность растеризации $maxRasterDurationMs мс',
)
],
),
),
);
}
void _onFrameEvent(FrameTiming timing) {
currentFrame = timing.frameNumber;
if (timing.buildDuration.inMilliseconds > maxBuildDurationMs) {
maxBuildDurationMs = timing.buildDuration.inMilliseconds;
}
if (timing.rasterDuration.inMilliseconds > maxRasterDurationMs) {
maxRasterDurationMs = timing.rasterDuration.inMilliseconds;
}
setState(() {});
}
}
Сборка мусора
С помощью сервиса SchedulerBinding можно управлять стратегией работы сборщика мусора.
PerformanceModeRequestHandle? requestPerformanceMode(DartPerformanceMode mode)
Метод просит его перейти в определённый DartPerformanceMode. Существуют четыре режима работы:
balanced— стандартный режим работы, идеально оптимизированный для Flutter;latency— снижение времени задержек за счёт увеличения накладных расходов на память; не рекомендуется находиться в этом режиме длительное время;throughput— увеличение пропускной способности за счёт увеличения задержек на обработку;memory— оптимизация для работы в условиях низкой доступной памяти, работает чаще с большим объёмом данных, что понижает перформанс.
На выходе вы получаете nullable-объект PerformanceModeRequestHandle, который используется для вывода из установленного DartPerformanceMode: нужно вызвать метод dispose для освобождения ресурсов. Если возвращается null, значит, в данный момент какой-то другой код запросил режим работы другого типа.
Используйте requestPerformanceMode для оптимизации только в том случае, если проблемы производительности приложения возникают из-за сборщика мусора. Помните: это крайняя мера, если другие оптимизации не помогли.
И ещё один совет: всегда замеряйте метрики производительности до и после изменений. Подробнее о производительности приложения мы расскажем в одном из дальнейших статей.
ServicesBinding
Вот за что он отвечает:
- Прослушивание и перенаправление платформенных сообщений в
BinaryMessenger, сервис, к которому по умолчанию привязываются платформенные каналы: каналы методов (MethodChannel) и событий (EventChannel). При получении очередного сообщенияBinaryMessengerперенаправляет его в соответствующий платформенный канал.BinaryMessengerумеет не только получать, но и отправлять сообщения в платформу. - Сбор и регистрация лицензий пакетов, которые были в приложении в качестве зависимостей. Лицензии пакетов зашиваются в приложении во время его сборки инструментами Flutter.
- Сохранение ссылки на токен главного изолята. Он может использоваться, если необходимо общаться через платформенные каналы из сторонних изолятов. Подробнее об этом вы прочитаете в статье Advanced изоляты и зоны, асинхронное и параллельное программирование.
- Обработка системных событий, которые идут от платформы. Например, запрос на выход из приложения, жизненный цикл приложения, событие out of memory, нажатия клавиатуры и др.
- Создание
RestorationManager— это сущность, которая отвечает за восстановление состояния приложения. Подробнее про него можно узнать в лекции Школы мобильной разработки Яндекса.
GestureBinding
Главная обязанность сервиса GestureBinding — это обработка взаимодействия пользователя с экраном устройства, то есть обработка жестов.
Получаемые на вход данные о нажатиях доставляются конкретным потребителям этих событий (кнопки, области со скроллом и та далее). Процесс распознавания адресата для события называется hitTest, результат распознавания — hitTestResult.
GestureBinding умеет кешировать hitTestResult для большей эффективности. Помимо обработки событий со стороны устройства, GestureBinding также открывает возможность посылать «ложные» события нажатий, что используется в TestWidgetsFlutterBinding.
Подробнее про hitTest мы поговорим в одном из следующих статей.
RendererBinding
Этот сервис — связующее между деревом RenderObject и Flutter engine. У него две основные обязанности:
- Прослушивание событий от
engineдля информирования об изменении настроек устройства, которые могут затрагивать семантический слой или как-то влиять на визуальное представление вашего приложения (например, тёмная тема или размер текста). - Передача во Flutter engine изменений на экране с помощью Layer tree. Подробнее об этом также расскажем в одном из следующих статей.
Чтобы передавать изменения в engine, этот сервис связи отвечает за управление PipelineOwner и инициализацию RenderView.
PipelineOwner — это такой объект, который знает, какой RenderObject должен среагировать в ответ на изменения layout. Он же и управляет этой реакцией.
SemanticsBinding
Связывает engine и слой семантики. Отвечает за всё необходимое для доступности (англ. ccessibility) приложения, чтобы им могли пользоваться люди с ограниченными возможностями здоровья:
- упрощение или отключение анимации;
- управление обновлениями семантики и доставка этих событий в
SemanticsNode, для этого используетсяSemanticsOwner; - обработка и доставка
SemanticsActionв нужныйSemanticsNode.
Подробнее про доступность мы также расскажем в одном из следующих статей.
PaintingBinding
Сервис для связи с библиотекой painting, вот за что он отвечает:
- механизм кеширования и вытеснения из кеша (cache eviction) изображений;
- прогрев шейдеров;
- уведомления об изменении шрифтов в системе и их предоставление;
- предоставление кодеков для декодирования изображений.
Вытеснение из кеша (cache eviction) — это процесс удаления данных из кеша компьютерной системы для освобождения места под новые данные. Кеш используется для временного хранения часто используемых данных и для быстрого доступа к ним. Однако кеш имеет ограниченный размер, и, когда он заполняется, новые данные не могут быть добавлены без удаления старых.
WidgetsBinding
Связывает engine и виджеты. У него две основные задачи:
- управление процессом перестроения структуры дерева элементов (для этого используется
BuildOwner); - вызов рендера в ответ на изменения структуры дерева.
Помимо этого, он объединяет функционал других сервисов связи и переадресовывает его в слушателей — виджеты с миксином WidgetsBindingObserver. Например, изменения состояния приложения AppLifeCycleState, которые изначально попадают в ServicesBinding, перехватываются и отправляются в WidgetsBindingObserver. Для того чтобы наблюдать за платформенными событиями, вам нужно примиксовать WidgetsBindingObserver в свой виджет.
Пример
import 'package:flutter/material.dart';
void main() => runApp(const WidgetBindingObserverExampleApp());
class WidgetBindingObserverExampleApp extends StatelessWidget {
const WidgetBindingObserverExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('App lifecycle observer')),
body: const WidgetBindingsObserverSample(),
),
);
}
}
class WidgetBindingsObserverSample extends StatefulWidget {
const WidgetBindingsObserverSample({super.key});
@override
State<WidgetBindingsObserverSample> createState() =>
_WidgetBindingsObserverSampleState();
}
class _WidgetBindingsObserverSampleState
extends State<WidgetBindingsObserverSample> with WidgetsBindingObserver {
final List<AppLifecycleState> _stateHistoryList = <AppLifecycleState>[];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (WidgetsBinding.instance.lifecycleState != null) {
_stateHistoryList.add(WidgetsBinding.instance.lifecycleState!);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_stateHistoryList.add(state);
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_stateHistoryList.isNotEmpty) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: ListView.builder(
itemCount: _stateHistoryList.length,
itemBuilder: (BuildContext context, int index) {
return AppLifecycleStateWidget(
text: _stateHistoryList[index].toString(),
);
},
),
),
);
}
return const Center(child: Text('Нет событий didChangeAppLifecycle'));
}
}
class AppLifecycleStateWidget extends StatelessWidget {
final String text;
const AppLifecycleStateWidget({
required this.text,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10.0),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.yellow, width: 2),
),
child: Center(
child: Text(
text,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}
Обратите внимание, что в методе dispose вызывается WidgetsBinding.instance.removeObserver(this) для освобождения памяти.
С помощью такого механизма работает виджет MediaQuery — он наблюдает за событием didChangeMetrics и сообщает подписчикам InheritedWidget про обновление.
WidgetsFlutterBinding
Этот сервис связи хоть и наследуется от BindingBase, но не отделяет в себе какую-то конкретную логику общения с engine. Его главная роль — инициализация всех сервисов связи, необходимых фреймворку для корректной работы.
TestWidgetsFlutterBinding
Содержит функционал, полезный при написании интеграционных тестов.
Используется библиотекой flutter_test. Как и WidgetsFlutterBinding, отвечает за инициализацию основных сервисов связи. Так же зависит от TestDefaultBinaryMessengerBinding, который переопределяет defaultBinaryMessenger на TestDefaultBinaryMessenger. Он имеет доступ к данным, отправленным со стороны плагинов, что полезно для тестовых фреймворков, мониторинга и синхронизации с сообщениями платформы.
Рендеринг и сервисы связи
Давайте вспомним с вами несколько фактов об устройстве вёрстки во Flutter, которые мы узнали ранее в этой главе, когда разбирали элементы:
- виджет — неизменяемая конфигурация для
Element; - из виджетов получается дерево элементов, элемент содержит ссылку на виджет, который его создал;
- элементы связаны друг с другом как parent и child;
- элемент может содержать
RenderObject.
Для того чтобы обновить картинку на устройстве, Element и RenderObject в начале проходят фазу аннулирования.
Аннулирование (англ. invalidate) — это проверка, что элементы или рендер-объекты не устарели. Например, при получении новой конфигурации элемент может ей не соответствовать, и тогда требуется обновление дерева элементов.
Для Element этот процесс запускается в следующих двух сценариях:
- Первый сценарий — в случае вызова метода
setState: проверяется, не устарел лиStatefulElement. - Второй сценарий — в случае, если
Elementподписан наProxyElement, который отправляет уведомление об изменении его конфигурации —InheritedWidget.
Результатом фазы аннулирования элементов является список элементов, помеченных флагом dirty.
Для RenderObject сценарии следующие:
- Изменения геометрии
RenderObject(позиция, размер и так далее). - Необходимость перерисовки (если поменялся только цвет, стиль шрифта и так далее).
В результате фазы аннулирования получается список RenderObject, который необходимо перерисовать.
После фазы аннулирования в ход вступает сервисSchedulerBinding и отправляет запрос в Flutter engine на планировку следующего кадра.
После того как engine будет готов отрисовать следующий кадр, он обращается к SchedulerBinding и вызывает метод onDrawFrame.
На схеме ниже показано, что происходит после получения SchedulerBinding сигнала onDrawFrame от engine:
Scheduler делегирует вызов в сервис WidgetsBinding, вызывая метод drawFrame.
В первую очередь WidgetsBinding рассматривает изменения, произошедшие в дереве элементов: вызывает уBuildOwner метод buildScope, в котором проходится список элементов, помеченных как dirty.
У каждого элемента вызывается метод rebuild, что, как правило, ведёт к вызову метода build и получению нового виджета. Далее есть два варианта поведения:
- Если у элемента не инициализировано поле
child, вызывается методinflate, что ведёт к созданию нового элемента. - Если поле инициализировано, происходит проверка по ключу и типу: можно ли оставить существующий элемент (child). Если оставить можно, то элемент остаётся, если нет — он выбрасывается, вызывается
inflateдля получения нового элемента на местоchild.
После обработки элементов подходит очередь рендер-объектов (RenderObject), которые требуют перерисовки. Сервис WidgetsBinding по цепочке вызывает метод drawFrame у RendererBinding, и происходит следующее:
- у каждого
RenderObject, помеченного dirty, вызывается методperformLayout, который считает геометрию объекта (размер, отступы и т. д.); - происходит перерисовка
RenderObject, у которого флагneedsPaintпринимает значение true; - полученная сцена отправляется в
RenderViewс помощью методаcompositeFrame, затем эта сцена доставляется воFlutter engineдля отрисовки; - затем происходит обновление слоя семантики.
Наконец, новый кадр появляется на экране устройства.
Совместив весь процесс отрисовки кадра, получаем следующую схему:
Знаем, было нелегко, но мы справились!
В этой статье мы узнали, что такое сервисы связи и как они «склеивают» Flutter engine с Flutter framework. Изучили, какие бывают типы сервисов связи и какую функцию выполняет каждый из них, а также познакомились с тем, какую роль они принимают в процессе рендеринга.
А в следующей статье мы начнём разговор об анимациях во Flutter.