Эта статья прямое продолжение моей предыдущей статьи.
Для полного восприятия советую предварительно ознакомиться с указанной по ссылке статьёй или освежить её в памяти. В ней я разобрал один из важных аспектов устройства Flutter — взаимодействие деревьев и распределение ответственности между ними. Однако, открытым остаётся вопрос: каким образом налажена работа всего механизма, описанного в первой части? В этом мы и постараемся разобраться в этой статье.
Если открыть технический обзор Flutter, в одном из пунктов мы увидим следующую схему. Она показывает условные уровни, на которые сами авторы фреймворка его делят.
По факту, как они сами это и назвали, мы видим слоёный пирог. Можно выделить более крупные и мелкие слои.
Уровень фреймворка — всё, с чем мы работаем в момент написания приложения, и все служебные классы, позволяющие взаимодействовать написанному нами с уровнем движка. Всё, относящееся к данному уровню написано на Dart.
Уровень движка — более низкий уровень, чем уровень фреймворка, содержит классы и библиотеки, позволяющие работать уровню фреймворка. В том числе виртуальная машина Dart, Skia и тд.
Уровень платформы — специфичные механизмы, относящиеся к конкретной платформе запуска.
Рассмотрим подробнее уровень фреймворка. На схеме он изображен в виде слоёв от более высокоуровневого к низкоуровневому. В самом низу мы видим слой Foundation. Исходя из названия, данный слой — нечто фундаментальное и основное на уровне фреймворка. Находим данную библиотеку и вот что написано в её описании:
Core Flutter framework primitives.
The features defined in this library are the lowest-level utility classes and functions used by all the other layers of the Flutter framework.
Функции, определённые в этой библиотеке, представляют собой служебные классы и функции самого низкого уровня, используемые всеми другими уровнями фреймворка Flutter.
В данной библиотеке, в том числе находится BindingBase — базовый класс для всех Binding.
Для начала давайте разберёмся, что же такое Binding и как он используется во Flutter. Само название говорит нам о том, что это некоторая связь. Документация, оставленная Flutter командой к BaseBinding сообщает нам следующее:
Base class for mixins that provide singleton services (also known as «bindings»). To use this class in an `on` clause of a mixin, inherit from it and implement [initInstances()]. The mixin is guaranteed to only be constructed once in the lifetime of the app (more precisely, it will assert if constructed twice in checked mode).
Это базовый класс для различных сервисов связи, которые представлены в виде миксинов. Каждый такой миксин инициализируется и гарантирует единственность своего экземпляра во время жизни приложения.
BaseBinding — базовый абстрактный класс, давайте тогда рассмотрим конкретные реализации биндингов. Среди них мы увидим:
ServicesBinding отвечает за перенаправление сообщений от текущей платформы в обработчик данных сообщений (BinaryMessenger);
PaintingBinding отвечает за связь с библиотекой отрисовки.
RenderBinding отвечает за связь между деревом рендеринга и движком Flutter.
WidgetBinding отвечает за связь между деревом виджетов и движком Flutter.
SchedulerBinding — планировщик очередных задач, таких как:
SemanticsBinding отвечает за связь слоя семантики и движком Flutter.
GestureBinding отвечает за работу с подсистемой жестов.
Как и следовало из названия, bindings — это прослойка-связь, между уровнем движка Flutter и уровнем самого фреймворка, каждый из которых отвечает за конкретное направление работы.
Чтобы лучше понять, как всё это работает вместе давайте посмотрим в место, которое является стартовым для любого Flutter приложения — вызов runApp. Метод, который мы вызываем, находится в файле binding.dart и это не случайно. В описании к нему сказано, что он расширяет переданный виджет приложения и прикрепляет его к экрану. Посмотрим, что он делает:
Здесь мы встречаем WidgetsFlutterBinding — конкретная реализация привязки приложений на основе инфраструктуры виджетов. По сути своей — это клей, соединяющий фреймворк и движок Flutter. WidgetsFlutterBinding состоит из множества связей, рассмотренных нами ранее: GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding. Таким образом мы получили прослойку, которая умеет связывать наше приложение по всем нужным направлениям с движком Flutter.
Вернёмся к запуску приложения. У WidgetsFlutterBinding вызываются методы scheduleAttachRootWidget и scheduleWarmUpFrame, рассмотрим что в них происходит.
Метод scheduleAttachRootWidget является отложенной реализацией attachRootWidget. Принадлежит данный метод WidgetsBinding. В описании к нему сказано, что он присоединяет переданный виджет к renderViewElement — корневому элементу дерева элементов.
По коду мы видим, что в этом методе создаётся адаптер RenderObjectToWidgetAdapter, который является корневым виджетом дерева виджетов и используется как мост, связывающий деревья между собой. Посмотрев в его реализацию, увидим, что он не создаёт RenderObject для себя, а возвращает значение из поля container, а в него мы при создании передаём renderView из RendererBinding. Данный renderView и есть корневой элемент дерева рендеринга.
RenderView get renderView => _pipelineOwner.rootNode;
PipelineOwner фактически является менеджером, который занимается управлением процесса рендеринга.
Вернёмся к методу attachRootWidget. У созданного адаптера вызывается метод attachToRenderTree, с помощью которого мы создаём первый раз корень дерева элементов. Стоит обратить внимание, что в метод attachToRenderTree передаётся buildOwner. Это класс менеджер билда дерева виджетов, который занимается отслеживанием состояния виджетов, отслеживает необходимость обновления и выполняет ряд других важных задач, связанных с актуализацией состояния дерева виджетов. Таким образом мы и получаем те самые три дерева Flutter, каждое из которых хранится и управляется через Bindings.
Метод scheduleWarmUpFrame принадлежит SchedulerBinding и используется для того, чтобы запланировать запуск кадра как можно скорее, не ожидая системного сигнала «Vsync». Поскольку метод используется во время запуска приложения, первый кадр, который, вероятно, будет довольно дорогим, будет запущен за некоторое дополнительное время. Данный метод блокирует отправку событий до завершения запланированного кадра.
Как мы видим, оба запущенных при старте механизма относятся и взаимодействуют с различными Bindings, которые в свою очередь тесно связаны и обеспечивают взаимодействие с системой. Давайте подытожим с помощью следующей диаграммы.
Bindings — это важный механизм организации работы приложения на Flutter. Именно он связывает различные аспекты работы приложения между собой, а также с движком.
В том числе благодаря ему мы можем писать наше приложение на самой высокоуровневой части фреймворка, не заботясь о том, как же нам организовать связную работу всего остального. Надеюсь данная статья поможет вам разобраться с этой частью устройства Flutter.
Спасибо за внимание!
Flutter
https://flutter.dev/
Для полного восприятия советую предварительно ознакомиться с указанной по ссылке статьёй или освежить её в памяти. В ней я разобрал один из важных аспектов устройства Flutter — взаимодействие деревьев и распределение ответственности между ними. Однако, открытым остаётся вопрос: каким образом налажена работа всего механизма, описанного в первой части? В этом мы и постараемся разобраться в этой статье.
Общие положения
Если открыть технический обзор Flutter, в одном из пунктов мы увидим следующую схему. Она показывает условные уровни, на которые сами авторы фреймворка его делят.
По факту, как они сами это и назвали, мы видим слоёный пирог. Можно выделить более крупные и мелкие слои.
Уровень фреймворка — всё, с чем мы работаем в момент написания приложения, и все служебные классы, позволяющие взаимодействовать написанному нами с уровнем движка. Всё, относящееся к данному уровню написано на Dart.
Уровень движка — более низкий уровень, чем уровень фреймворка, содержит классы и библиотеки, позволяющие работать уровню фреймворка. В том числе виртуальная машина Dart, Skia и тд.
Уровень платформы — специфичные механизмы, относящиеся к конкретной платформе запуска.
Рассмотрим подробнее уровень фреймворка. На схеме он изображен в виде слоёв от более высокоуровневого к низкоуровневому. В самом низу мы видим слой Foundation. Исходя из названия, данный слой — нечто фундаментальное и основное на уровне фреймворка. Находим данную библиотеку и вот что написано в её описании:
Core Flutter framework primitives.
The features defined in this library are the lowest-level utility classes and functions used by all the other layers of the Flutter framework.
Функции, определённые в этой библиотеке, представляют собой служебные классы и функции самого низкого уровня, используемые всеми другими уровнями фреймворка Flutter.
В данной библиотеке, в том числе находится BindingBase — базовый класс для всех Binding.
Binding
Для начала давайте разберёмся, что же такое Binding и как он используется во Flutter. Само название говорит нам о том, что это некоторая связь. Документация, оставленная Flutter командой к BaseBinding сообщает нам следующее:
Base class for mixins that provide singleton services (also known as «bindings»). To use this class in an `on` clause of a mixin, inherit from it and implement [initInstances()]. The mixin is guaranteed to only be constructed once in the lifetime of the app (more precisely, it will assert if constructed twice in checked mode).
Это базовый класс для различных сервисов связи, которые представлены в виде миксинов. Каждый такой миксин инициализируется и гарантирует единственность своего экземпляра во время жизни приложения.
BaseBinding — базовый абстрактный класс, давайте тогда рассмотрим конкретные реализации биндингов. Среди них мы увидим:
ServicesBinding отвечает за перенаправление сообщений от текущей платформы в обработчик данных сообщений (BinaryMessenger);
PaintingBinding отвечает за связь с библиотекой отрисовки.
RenderBinding отвечает за связь между деревом рендеринга и движком Flutter.
WidgetBinding отвечает за связь между деревом виджетов и движком Flutter.
SchedulerBinding — планировщик очередных задач, таких как:
- вызовы приходящих колбеков, которые инициирует система в Window.onBeginFrame — например события тикеров и контроллеров анимаций;
- вызовы непрерывных колбеков, которые инициирует система Window.onDrawFrame, например, события для обновления системы отображения после того, как отработают приходящие колбеки;
- посткадровые колбеки, которые вызываются после непрерывных колбеков, перед возвратом из Window.onDrawFrame;
- задачи не связанные с рендерингом, которые должны быть выполнены между кадрами.
SemanticsBinding отвечает за связь слоя семантики и движком Flutter.
GestureBinding отвечает за работу с подсистемой жестов.
Как и следовало из названия, bindings — это прослойка-связь, между уровнем движка Flutter и уровнем самого фреймворка, каждый из которых отвечает за конкретное направление работы.
WidgetsFlutterBinding
Чтобы лучше понять, как всё это работает вместе давайте посмотрим в место, которое является стартовым для любого Flutter приложения — вызов runApp. Метод, который мы вызываем, находится в файле binding.dart и это не случайно. В описании к нему сказано, что он расширяет переданный виджет приложения и прикрепляет его к экрану. Посмотрим, что он делает:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
Здесь мы встречаем WidgetsFlutterBinding — конкретная реализация привязки приложений на основе инфраструктуры виджетов. По сути своей — это клей, соединяющий фреймворк и движок Flutter. WidgetsFlutterBinding состоит из множества связей, рассмотренных нами ранее: GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding. Таким образом мы получили прослойку, которая умеет связывать наше приложение по всем нужным направлениям с движком Flutter.
Вернёмся к запуску приложения. У WidgetsFlutterBinding вызываются методы scheduleAttachRootWidget и scheduleWarmUpFrame, рассмотрим что в них происходит.
ScheduleAttachRootWidget
Метод scheduleAttachRootWidget является отложенной реализацией attachRootWidget. Принадлежит данный метод WidgetsBinding. В описании к нему сказано, что он присоединяет переданный виджет к renderViewElement — корневому элементу дерева элементов.
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement);
}
По коду мы видим, что в этом методе создаётся адаптер RenderObjectToWidgetAdapter, который является корневым виджетом дерева виджетов и используется как мост, связывающий деревья между собой. Посмотрев в его реализацию, увидим, что он не создаёт RenderObject для себя, а возвращает значение из поля container, а в него мы при создании передаём renderView из RendererBinding. Данный renderView и есть корневой элемент дерева рендеринга.
RenderView get renderView => _pipelineOwner.rootNode;
PipelineOwner фактически является менеджером, который занимается управлением процесса рендеринга.
Вернёмся к методу attachRootWidget. У созданного адаптера вызывается метод attachToRenderTree, с помощью которого мы создаём первый раз корень дерева элементов. Стоит обратить внимание, что в метод attachToRenderTree передаётся buildOwner. Это класс менеджер билда дерева виджетов, который занимается отслеживанием состояния виджетов, отслеживает необходимость обновления и выполняет ряд других важных задач, связанных с актуализацией состояния дерева виджетов. Таким образом мы и получаем те самые три дерева Flutter, каждое из которых хранится и управляется через Bindings.
ScheduleWarmUpFrame
Метод scheduleWarmUpFrame принадлежит SchedulerBinding и используется для того, чтобы запланировать запуск кадра как можно скорее, не ожидая системного сигнала «Vsync». Поскольку метод используется во время запуска приложения, первый кадр, который, вероятно, будет довольно дорогим, будет запущен за некоторое дополнительное время. Данный метод блокирует отправку событий до завершения запланированного кадра.
Как мы видим, оба запущенных при старте механизма относятся и взаимодействуют с различными Bindings, которые в свою очередь тесно связаны и обеспечивают взаимодействие с системой. Давайте подытожим с помощью следующей диаграммы.
Заключение
Bindings — это важный механизм организации работы приложения на Flutter. Именно он связывает различные аспекты работы приложения между собой, а также с движком.
В том числе благодаря ему мы можем писать наше приложение на самой высокоуровневой части фреймворка, не заботясь о том, как же нам организовать связную работу всего остального. Надеюсь данная статья поможет вам разобраться с этой частью устройства Flutter.
Спасибо за внимание!
Используемые материалы:
Flutter
https://flutter.dev/
dolpheen
Спасибо за серию интересных статей о подкапотном пространстве Flutter!
На счет слоя «Framework» SDK — с точки зрения архитектуры, он как две капли воды похож на веб-систему, например: «деревья» webkit — HTML/DOM, HTMLElement, RenderObject/RenderLayer, удивительным образом напоминают структуру фреймворка Flutter — Widget (Immutable Widget, что очень хорошо связано по смыслу с описанием посредством HTML языка), Element, RenderObject/Layer (тут даже названия практически совпадают), не упоминая Skia, который является общим движком для Chrome и Flutter. Что не удивительно, поскольку одну из основных ролей разработке Flutter ведёт, в том числе, Ian Hickson (Hixie) — гуру и активный участник HTML сообщества.
Попытки связать веб и натив как есть (Cordova, Xamarin, RN) с точки зрения текущих возможностей (в сторонке стоит QML который решил зайти с другой стороны) думаю вполне себя зарекомендовали в определенных нишах, но Flutter видится мне как следующий шаг в этом направлении — который похож на трансформацию существующих web технологий основанных на HTML, Javascript, CSS в нечто большее.
P.S. Возможно этот комментарий подходит больше к первой части, но, поскольку я «новобранец» на хабре, возможность оставить есть только здесь.