Заманчивая идея – написал код один раз, и он работает на всех платформах. По-моему, у Java это не плохо получилось. Дополнительная абстракция в виде байт-кода и JVM, отделяющая приложение от особенностей реализации платформы, помогла Java сделать своё дело.  В 1995 году, когда компания Sun провозгласила этот лозунг, ещё никто не знал что такое смартфон, а web-технологии только только начинали покорять мир. Похоже, с тех пор, многое изменилось. Количество IT-продуктов, как и их пользователей, росло огромными темпами, соответственно выросло количество технологий, инструментов и языков разработки. Акцент взаимодействия конечных пользователей с приложениями сместился в сторону web и мобильных платформ, а Android и iOS практически единолично поделили между собой мир мобильных устройств. Тем временем, можно определённо сказать, что лозунг "напиши один раз и запускай везде" приобрёл еще большую актуальность.  C точки зрения бизнеса, единая кодовая база, на которой можно собрать приложение с минимальными изменениями или дополнениями для различных платформ, звучит соблазнительно. Важную роль сегодня играет UI/UX, возможности для реализации которых, нам предоставляют web и мобильные платформы. И если, в плане мультиплатформенности бизнес-логики, к Java вопросов нет, то, например, про Swing думаю можно не обсуждать. Перед техническим погружением в реализацию механизмов отрисовки во Flutter, вкратце, опишу своё мнение о текущем состоянии мультиплатформы. Web-технологии не стоят на месте - HTML5, WebGL, WebRTC, WebAssembly, Progressive Web App. Казалось бы, идеальный кандидат для кроссплатформы, с виртуальной машиной (браузером), которая присутствует почти в каждом девайсе пользователя, от компьютера и телефона до часов и телевизора. Много внимания уделяется Responsive design в вебе, он помогает справиться с различными размерами экранов, а PWA делает web-приложения всё ближе к полноценным нативным приложениям. Для мобильных платформ предлагаются "промежуточные" решения, например, обернуть WebView в нативный код, чтобы притвориться нормальным приложением. PhoneGap/Cardova, Ionic и многие другие из этой группы, как говорится, - Добро пожаловать, web-разрабочики, в мир мобильных приложений! Есть желание использовать возможности платформы, которых пока нет в реализации браузера? не беда, подключаем плагины. Такой подход существует уже с 2005 года, но, по-видимому, не всё так гладко. Идея! - сказал кто-то в Facebook, - Берём полюбившийся сообществу веб-разработчиков ReactJS, а чтобы пользовательский опыт не сильно страдал, будем использовать "родные" для платформы интерфейсы взаимодействия с пользователем - на сцене появляется React Native. Xamarin c C#, KMM с Kotlin, по-моему, можно причислить к этой же когорте. Как бы там ни было - React Native, с Javascript и часто обсуждаемым "мостом", занял вполне достойное место в ряду кроссплатформенных решений. В 2014 году, тихо и без пафоса, стартовал проект Sky, вскоре появляется первая публичная информация. А, в 2018 году, уже под именем Flutter, проект достаточно громко заявил о себе выпуском первой стабильной версии. К тому времени, у меня уже был опыт использования ReactNative, PhoneGap/Ionic и PWA - и вроде всё работает, бизнес-задачи решаются, но, где-то внутри, было труднообъяснимое чувство недосказанности по этим инструментам. Когда я впервые услышал о Flutter, первая мысль - очередная попытка "поженить" web и mobile, но, после более глубокого знакомства с работой фреймворка, понял, что ошибался, понял, что меня не устраивало в предыдущих решениях - вот эти попытки, связать по живому mobile и web, как Франкенштейна.  Flutter выглядел одновременно новым, и в тоже время знакомым решением. Новым - потому что увидел смелый шаг с отказом от полумер, в попытке "угодить и этим и этим", знакомым - поскольку, при изучении фреймворка, часто наталкивался на известные и хорошо себя показавшие подходы из веб-технологий, что не удивительно, ведь вдохновителями Flutter были и остаются Eric Siedel и Ian Hickson, которые принимали непосредственное участие в разработке Chrome(WebKit, Blink), HTML5 и других web-технологий,  а разработчик Dart, Вячеслав Егоров, принимал активное участие в создании Javascript V8. Мне кажется, эти люди просто решили сделать шаг вперед, без оглядки на взаимные ограничения накладываемые web и mobile. Просто настало время, появились люди и поддержка компании Google)) Хорошая это была идея или плохая, покажет время. А пока, при упоминании Flutter, число классических мнений: «кладбище проектов google» и «зачем с нуля изобретать очередной велосипед» уменьшается пропорционально росту количества приложений на Flutter, вакансий и популярности проекта на GitHub, и социальной активности в общем, а появление SwiftUI и Jetpack Compose (и более экзотических) подтверждает жизнеспособность декларативного подхода при определении UI в коде.   В этой статье, я бы хотел вместе с вами погрузиться в исследование механизмов работы, одного из самых важных компонентов  Flutter - отрисовка и рендеринг. И, заодно, попробуем понять, что такое шейдеры, которые последнее время на слуху в сообществе разработчиков Flutter, и почему они влияют на лаги анимации при первом старте. Идея использования слоёв в IT-архитектуре, не нова, взять ту же Clean Architecture, которая позволяет нам разделить ответственности и взаимодействие между слоями. Flutter с самого начала своей истории активно использует этот подход. В первом приближении Flutter UI Toolkit делится на 3 слоя: Application Code - код написанный разработчиком Flutter Flutter Framework - собственно сам "фреймворк", который, обычно, подразумевают при упоминании Flutter, написан на Dart, содержит большое количество базовых виджетов для построения визуального интерфейса взаимодействия с пользователем, механизм построения и компоновки виджетов, создание команд отрисовки готовой сцены для передачи на уровень Engine, инструменты тестирования и отладки. Flutter Engine - платформонезависимая часть Flutter, написанная на C/C++, фактически представляет из себя динамическую библиотеку, подключаемую при старте приложения. Поставляется в скомпилированном виде на машине разработчика при установке Flutter. Содержит виртуальную машину Dart, библиотеку Skia, код рендеринга,  код взаимодействия с нижнем уровнем платформы. Embedder - часть реализации, зависящая от платформы, для которой будет собираться приложение, отвечает за: подготовку и предоставление поверхности для рисования в Engine, сигнал VSYNC(синхронизация отрисовки кадра),  обслуживание событий интерфейса пользователя, создание потоков и внутренних очередей событий. Это первый слой, с которым начинает "общаться" Flutter разработчик. А если быть более точным, и принять во внимание отличие понятий "библиотека" и  "фреймворк", Flutter Framework, управляет написанным нами кодом. На языке Dart, создается декларативное описание интерфейса в виде древовидной компоновки объектов Widget, и уже сам фреймворк вызывает методы отрисовки или перестроения дерева виджетов. Сам фреймворк написан полностью на языке Dart, и компилируется вместе с нашей программой в момент сборки приложения. Одна из основных задач фреймворка - преобразовать описаный нами декларативный интерфейс, в так называемую "сцену", и отправить на слой ниже в Engine. Фреймворк также внутри разделен на слои, каждый со своей зоной ответственности.  Уровень виджетов - использует программист Flutter при разработке(чуть ниже есть уровень Elements, его пропустим, полное описание принципа построения виджетов будет дано по ссылке ниже) Уровень рендеринга - сам фреймворк, под капотом, использует так называемые RenderObject, которые формируются из дерева виджетов  Уровень отрисовки - здесь на основе дерева RenderObject формируются команды отрисовки для передачи в Engine. Для исследования, возьмем простейший пример, состоящий из нескольких виджетов с простой конфигурацией. По коду нашего приложения видно, что мы, для отображения нашей композиции, не указывали конкретных координат отрисовки, положение и направлений линий и закраски, нет каких-либо команд рисования, т.е. в данном случае используем полностью декларативный, описательный подход. Также во  Flutter, в качестве размеров используются логические пиксели, которые не зависят от реального разрешения и плотности экрана и позволяют оперировать практически одинаковыми визуальными размерами на разных устройствах. После запуска приложения происходит инициализация так называемых Bindings, которые связывают фреймворк с нижним уровнем Engine.  SchedulerBinding   Отвечает, в том числе, за обработку и формирование вызовов сигнализирующих о текущем статусе построения фрейма (BeginFrame, DrawFrame) WidgetsBinding отвечает за перестроение дерева виджетов, построение elements и RenderObejct RenderBinding После построения дерева элементов, RenderObject и их взаимосвязей, в работу включается слой фреймворка Rendering, который выполняет несколько этапов Layout - производится расчет размеров и положения всех RenderObject CompositingBits -  обновляется информация об объетках использующих композицию Paint - отрисовка экрана. Здесь задействована Skia, через использование PictureLayer, PictureRecorder и Canvas, RenderObjects отрисовываютс себя с помощью этих элементов, но, на самом деле фактически просто формируется список команд отрисовки и применения слоёв. Compositing - создается и строится сцена Scene с учетом всех слоёв их композиции и команд отрисовки на этих слоях. После того как сцена(Scene) со всеми командами отрисовки подготовлена, она передается на уровень Engine с помощью метода render. Затем из сцены извлекается LayerTree, производятся необходимые предварительные действия и проверка дерева. На данном этапе, работа в потоке UI завершается, а LayerTree передается на обработку в Raster поток. При запуске Flutter приложения, инициализируются четыре потокаUI Thread - основная программа пользователя и код фреймворка на DartRaster Thread - поток работы с OpenGL и отрисовкой на GPUIO Thread - вспомогательный поток для Raster thread, обработка assets  Platform Thread - платформозависимый код приложения Процессом непосредственной отрисовки на экран устройства управляет класс Rasterizer. Метод Draw этого класса запускается в отельном потоке Raster. Сначала в работу вступает композитор Flow, он разворачивает дерево и подготавливает слои дерева (LayerTree:Preroll) к отрисовке, а после выполняет команды отрисовки Skia, которые находятся в дереве (LayerTree:paint), замечу, в данном случае непосредственно отрисовки на экране не происходит, по сути собирается очередь из так называемых объектов операций Skia (см. addDrawOp) В методе SkCanvas:Flush производится выполнение операций Skia, из очереди, которая была подготовлена на предыдущем этапе, эти операции выполняют непосредственную отрисовку в OpenGL буфер.  После отрисовки вызывается метод платформы SwapBuffer, изображение появляется на экране, а цикл рендеринга фрейма в потоке GPU завершается. По временному графику видно, что время подготовки сцены в UI потоке и её последующей композиции в Raster потоке мало, по сравнению с временем выполнения непосредственной отрисовки в SkCanvas:Flush. В нашем случае простой сцены это в пределах нормы, но при сложной сцене время отрисовки может значительно возрасти. Как правило, это связно с подготовкой программ для графического процессора по каждой из операций Skia. Обратите внимание на пару GrGlProgram в операциях отрисовки прямоугольников из нашего примера - FillRectOp на графике, это как раз место где генерируются и компилируются программы для GPU.  Ше́йдер (англ. shader «затеняющий») — компьютерная программа, предназначенная для исполнения процессорами видеокарты (GPU). Шейдеры составляются на одном из специализированных языков программирования  и компилируются в инструкции для ЦП. Skia, как и большинство программ использующих OpenGL/Metal при рендеринге, не использует команды отрисовки отдельных точек, линий, полигонов и их закрашивание. Для отрисовки элемента сначала создаются буферы с вершинами и их координатами(vertex), затем генерируются так называемые шейдеры. Шейдеры - по сути программы, которые работают на процессорах GPU, и параллельно обрабатывают координаты и цвета точек, которые им были предварительно переданы через буферы.  В Skia, для рендеринга элементов используются 2 типа шейдеров - это вершинные(vertex) и пиксельные(fragment). Вершинные работают с координатами(например могут применять матрицы трансформации, проекции), пиксельные управляют цветом каждого пикселя перед выдачей на экран устройства. Существуют несколько вариантов языков описания шейдеров, в большинстве своём, это Си-подобные языки (Metal например использует спецификацию С++14). Чтобы абстрагироваться от разных вариантов языков, Skia, под капотом, использует свой формат SkSL, который фактически унаследован от OpenGL GLSL. Ниже приведены примеры шейдеров, которые создаются  для нашей простейшей программы, с изображением рамки со скруглёнными углами и закрашенного квадрата.   Т.е. в нашем примере сгенерировано всего 2 шейдера, но если мы возьмем классический пример приложения каунтера во Flutter: шейдеров гораздо больше (с учётом эффекта при нажатии кнопки)  Поскольку шейдеры, по сути, просто программы на исходном языке, как и обычные программы на С, перед использованием на GPU их необходимо скомпилировать.  Конечно для компиляции требуется время, поэтому, как правило, существует два варианта - или компилировать непосредственно перед использованием в процессе рендеринга очередного кадра, или компилировать их предварительно, перед запуском основного кода приложения - подход часто используемый, например, для игровых приложений. Здесь и проявляется проблема с пропуском кадров во время первых анимаций, которую сейчас горячо обсуждают в сообществе Flutter. По-умолчанию, в Skia, шейдеры компилируются перед использованием, если размер шейдеров небольшой, то это никак не влияет на поведение, а если, например, при анимации роута, на новом экране будут использоваться различные виды графических примитивов, которые потребуют значительное время на компиляцию шейдеров, то, в это время, текущая анимация может подлагивать. Можно ли предварительно скомпилировать все возможные шейдеры, перед запуском программы? Шейдеры в Skia генерируются в зависимости от конфигурации операции - например, закрашенный прямоугольник или только рамка, стиль отрисовки, применение матриц трансформаций - соответственно это порождает много возможных вариантов для каждого примитива (насколько много, на текущий момент, я сказать не могу, предположительно, с учетом всех примитивов, речь идет о сотнях)  Каждой программе присваивается уникальный ключ, и после компиляции шейдера, происходит сохранение результатов компиляции по этому ключу. В следующий раз, при формировании такой же программы и условии наличия в кэше ключа, повторная компиляция будет не нужна, поэтому данная проблема проявлятся только при первой анимации. Для решения проблемы компиляции большого количества шейдеров в OpenGL, команда Flutter и Skia, реализовала метод предварительной записи шейдеров  - так называемый "прогрев шейдеров". Суть метода такова - на машине разработчика запускается приложение с ключом, который позволяет сохранять используемые в Skia шейдеры. После запуска приложения, мы пробуем все варианты взаимодействия с интерфейсом, при этом шейдеры запоминаются и сохраняются на диск. Приложение собирается с этими шейдерами, и при старте на устройстве пользователя, они считываются и компилируются перед запуском основного кода. Сам формат в котором Flutter сохраняет шейдеры закодирован с помощью base64, поэтому для удобства анализа я написал небольшую программу, которая может декодировать в читабельный вид. Ссылка на программу здесь. Плагин VSCode для подсветки синтаксиса здесь. Выход metal и добровольно-принудительный переход на эту библиотеку в приложениях iOS, обострил проблему компиляции с новой силой. Дело в том, что предлагаемый метод "прогрева шейдеров", был реализован только на уровне OpenGL. Кроме того, текущий подход при компиляции шейдеров Metal MLSL, в некоторых случаях занимает больше времени, чем, при равных условиях, компиляция шейдеров OpenGL GLSL, что также повлияло на юзер экспириенс и справедливые возмущения разработчиков и бизнеса. Подход к обработке скомпилированных объектов и их применении в пайплайне рендеринга Metal, немного отличаются от подхода в OpenGL, но, в любом случае методы предварительной компиляции описаны и доступны для использования. Также осенью прошлого года, Apple на WWDC2020 провела презентацию по использованию прекомпиляции и бинарных файлов при построении пайплайна GPU. По моему мнению, эта проблема "первых кадров" лежит не в технической, а организационной плоскости, и команда Flutter не придавала должного значения анализу данной проблемы, а сама проблема решалась без привлечения специалистов по Metal. И только накал страстей и давление сообщества, помогло сдвинуть дело с мертвой точки, и последние недели на GitHub Skia, стали появляется изменения связанные с оптимизацией бинарных архивов Metal и подготовкой библиотек шейдеров. Также необходимо упомянуть, что команда Flutter рассматривает вариант предварительной записи и "прогрева шейдеров" как временный workaround. Конечная цель - полностью избавить разработчика от переживаний и дополнительных действий связанных с шейдерами. Буквально пара слов про рендеринг на Canvas в вебе. Для рендеринга, HTML элемент сanvas, используется только для инициализации WebGL (по сути, калька с OpenGLES). Skia, как известно, хорошо работает с OpenGL. Поэтому фактически библиотека Skia написанная на C++, компилируется в WebAssembly с помощью Emscripten + добавляются некоторые байндинги и подключается к проекту. Как видно Flutter пытается откусить достаточно жирный кусок у платформы, от неё, по-сути, требуется только поверхность для рисования, и некоторые системные вещи, и, сейчас, Flutter стабильно поддерживает Android/iOS/Linux/Windows/Web. Конечно, такой кардинальный подход, который выбрала Flutter команда, не обходится без трудностей, но куда ж без них, без трудностей)) Flutter architectural overviewInside FlutterSkia Flutter Enthusiast