Меня зовут Андрей, я Flutter-разработчик в команде Центра развития финансовых технологий (ЦРФТ) Россельхозбанка. Сегодня поговорим о «jank shaders» — дёргающейся анимации при первых запусках приложений на Flutter — и о том, как можно постараться её исправить.

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

На скриншоте ниже приведен пример задержки анимации перехода между экранами при первом запуске.

Первый запуск. github.com/flutter/flutter/issues/61450
Первый запуск. github.com/flutter/flutter/issues/61450

Основная (но не единственная) причина такого явления — время компиляции шейдеров. Так как, по сути, шейдер — это программа, которая работает на GPU (графическом процессоре), то перед первым использованием её необходимо скомпилировать на устройстве. Компиляция может занять до нескольких сотен миллисекунд, в то время как плавный кадр должен быть отрисован в течение 16 миллисекунд для отображения с частотой 60 кадров в секунду. Таким образом, долгая компиляция может привести к пропуску большого количества кадров, что приводит к задержкам в анимации.

В отличие от нативных платформ IOS и Android, которые намеренно используют небольшое количество шейдеров для анимации базовых элементов, Flutter предоставляет разработчикам возможность создавать произвольные анимации и эффекты за счет запуска их на GPU. Кроме того, в отличие от игровых движков, Flutter компилирует шейдеры непосредственно при первом использовании, а не при запуске приложения (откуда и берутся экраны загрузки). После нескольких таких запусков пропадают задержки на Android. Однако на IOS после включения во Flutter поддержки Metal #17431 разработчики непреднамеренно потеряли один из слоев кэширования шейдеров из-за отсутствия его поддержки в Metal/Skia, что привело к рывкам в анимации каждый раз при запуске приложения.

Второй запуск. github.com/flutter/flutter/issues/61450
Второй запуск. github.com/flutter/flutter/issues/61450
Третий запуск. github.com/flutter/flutter/issues/61450
Третий запуск. github.com/flutter/flutter/issues/61450

Проблема задержки анимации коснулась фундаментального уровня и потребовала внесения изменений не только в сам Flutter, но и в Skia, из-за чего разработчики некоторое время не уделяли ей должного внимания.

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

В течении нескольких релизных итераций разработчик произвел улучшения производительности в самом Flutter. 

Так например начиная с Flutter 2.5 появилась возможность подключения предварительной компиляции шейдера для Metal из обучающих прогонов #25644, что сокращает время растеризации кадра от 2/3 с до 1/2 с.

Также, начиная с этой версии, обработка асинхронных событий из сети, файловой системы, плагинов или других изолятов, прерывающих анимацию, была улучшена. Обработка кадров стала иметь больший приоритет над обработкой других асинхронных событий #25789 после изменения политик планирования. Также была улучшена работа сборщика мусора для освобождения памяти ( #26219 , #82883 ) и работа каналов в связке Dart и Objective-C/Swift (iOS) и Dart и Java/Kotlin (Android), что сократило задержки до 50% (Improving Platform Channel Performance in Flutter).

Начиная с версии 2.8 появляются удобные инструменты отладки производительности приложения Enhance tracing, а с версии 3.0 появляется и Flutter Impeller.

Сцены во Flutter

Для того чтобы нарисовать что-либо на экране, Skia нужна сцена (scene). Из сцены извлекается Layer Tree, выполняются развертка и подготовка шейдеров, которые далее компилируются для выполнения графическим процессором.

Сцена подготавливается непосредственно Flutter Framework, после построения дерева Render Objects и прохода этапов рендеринга (Layout, Paint, Compositing).

Render Object имеет метод paint(PaintContext context, Offset offset).  Внутри которого идет отрисовка на холсте примитивами вида context.canvas.drawRect() или context.canvas.drawLine() и т.д.

Но мы можем отобразить что-нибудь на экране, не прибегая ко всей мощи, скорости и оптимизациям Flutter, без использования Widget, Element и Render Object. Ниже приведен пример того, как мы можем нарисовать что-нибудь на экране, просто передав готовую сцену на уровень Engine в Skia.

void main() {
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final paint = Paint();
paint.color = Colors.blue;
paint.strokeWidth = 3.0;
canvas.drawLine(const Offset(100, 100), const Offset(500, 500), paint);
canvas.drawLine(const Offset(500, 500), const Offset(100, 900), paint);
final picture = recorder.endRecording();
final sceneBuilder = SceneBuilder()
..pushOffset(0, 0)
..addPicture(Offset.zero, picture)
..pop();
window.render(sceneBuilder.build());
}

Создав PictureRecorder, мы передаем его в конструктор Canvas, где производим отрисовку примитивов на холсте, после её окончания с помощью метода endRecording() мы получаем объект Picture, нужный для создания сцены. И затем непосредственно создаем сцену с помощью функции build, предварительно добавив туда объект Picture. 

Если мы хотим обновить экран, то нам нужно привязаться к коллбеку PlatformDispatcher.onBeginFrame или PlatformDispatcher.onDrawFrame.

Каждое приложение Flutter начинается с корневого Render Object - RenderView, у которого как раз и есть вызов window.render(sceneBuilder.build()) в методе compositeFrame. Window это синглтон, который образуется в виде создания инстанса класса SingletonFlutterWindow расширяясь FlutterWindow, а тот в свою очередь FlutterView. В FlutterView как раз находится  метод render(Scene scene), предназначение которого заключается в передаче сцены на уровень Engine в Skia. Затем на экране отобразятся две голубые линии.

Skia Shading Language

Ше́йдер (англ. shader «затеняющий») — компьютерная программа, предназначенная для исполнения процессорами видеокарты (GPU). Шейдеры составляются на одном из специализированных языков программирования  и компилируются в инструкции для графического процессора.

SkSL («язык шейдеров Skia») — это вариант GLSL, который используется в качестве языка Skia. SkSL, по сути, представляет собой единую стандартизированную версию GLSL, которая позволяет абстрагироваться от существующих различных вариантов языков описания шейдеров. Skia использует компилятор SkSL для преобразования кода SkSL в GLSL, GLSL ES или SPIR-V.

"Прожиг шейдеров" во Flutter

Flutter предоставляет разработчикам возможность через командную строку собирать шейдеры, которые могут понадобиться в дальнейшем в формате SkSL (Skia Shader Language). Затем шейдеры SkSL можно упаковать в приложение и «прогреть» (принудительно скомпилировать).

Как это сделать?

  1. Запускаем сборку в режиме profile

flutter run --profile --cache-sksl —purge-persistent-cache;
  1. Запускаем проблемную анимацию на устройстве

  2. Нажимаем M (запись) и выход: q;

  3. Компилируем сборку для Android 

flutter build apk --bundle-sksl-path flutter_01.sksl.json

или iOS

flutter build ios --bundle-sksl-path flutter_01.sksl.json.

Flutter Impeller

Начиная с версии Flutter 3.0 появилась возможность включить Impeller во время сборки приложения #100835. Impeller производит компиляцию более простых и мелких шейдеров не при первом использовании, а при сборке приложения. Шейдеры создаются один раз в GLSL 4.60 и конвертируются по мере необходимости. Во время сборки компилятор шейдеров impellerc преобразует GLSL в SPIR-V.

Целью SPIR-V является естественное представление примитивов, необходимых для вычислений и графики; отделить язык высокого уровня от интерфейса до вычислительных и графических драйверов; быть формой распространения или распространять полностью скомпилированные двоичные файлы; быть полностью автономной спецификацией...

Далее SPIR-V транслируется в специфичный для платформы высокоуровневый язык шейдинга. Сгенерированные таким путем файлы компилируются, оптимизируются и связываются в один двоичный объект.

Как использовать Impeller

Для iOS в свой файл Info.plist внутри <dict> нужно добавить:

<key>FLTEnableImpeller</key>
 <true/>

Для Android в свой AndroidManifest.xml внутри <application>:

<meta-data android:name="io.flutter.embedding.android.EnableImpeller" 
android:value="true"

Impeller будет использоваться при сборке вашего приложения для IOS и Android. Он доступен на этих операционных системах, поддержка веб в ближайшее время пока не планируется.

По заявлениям разработчиков текущая версия Impeller находится в стадии разработки и является прототипом, так что следует использовать её с осторожностью.

Заключение

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

Комментарии (2)


  1. alexxisr
    03.08.2022 11:26
    +1

    А без шейдеров и ГПУ, да и вобще без анимации банковское приложение уже неюзабельное?


    1. Anocean Автор
      03.08.2022 11:50
      +3

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