Меня зовут Андрей, я Flutter-разработчик в команде Центра развития финансовых технологий (ЦРФТ) Россельхозбанка. Сегодня поговорим о «jank shaders» — дёргающейся анимации при первых запусках приложений на Flutter — и о том, как можно постараться её исправить.
Несколько лет назад пользователи отметили, что при первом запуске приложений на Android и IOS наблюдаются некоторые задержки в анимации, но при последующих запусках задержки постепенно исчезают. Подобная проблема распространяется не только на пользовательские анимации, но и на базовые, такие как например переходы между экранами.
На скриншоте ниже приведен пример задержки анимации перехода между экранами при первом запуске.
Основная (но не единственная) причина такого явления — время компиляции шейдеров. Так как, по сути, шейдер — это программа, которая работает на GPU (графическом процессоре), то перед первым использованием её необходимо скомпилировать на устройстве. Компиляция может занять до нескольких сотен миллисекунд, в то время как плавный кадр должен быть отрисован в течение 16 миллисекунд для отображения с частотой 60 кадров в секунду. Таким образом, долгая компиляция может привести к пропуску большого количества кадров, что приводит к задержкам в анимации.
В отличие от нативных платформ IOS и Android, которые намеренно используют небольшое количество шейдеров для анимации базовых элементов, Flutter предоставляет разработчикам возможность создавать произвольные анимации и эффекты за счет запуска их на GPU. Кроме того, в отличие от игровых движков, Flutter компилирует шейдеры непосредственно при первом использовании, а не при запуске приложения (откуда и берутся экраны загрузки). После нескольких таких запусков пропадают задержки на Android. Однако на IOS после включения во Flutter поддержки Metal #17431 разработчики непреднамеренно потеряли один из слоев кэширования шейдеров из-за отсутствия его поддержки в Metal/Skia, что привело к рывкам в анимации каждый раз при запуске приложения.
Проблема задержки анимации коснулась фундаментального уровня и потребовала внесения изменений не только в сам 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 можно упаковать в приложение и «прогреть» (принудительно скомпилировать).
Как это сделать?
Запускаем сборку в режиме profile
flutter run --profile --cache-sksl —purge-persistent-cache;
Запускаем проблемную анимацию на устройстве
Нажимаем M (запись) и выход: q;
Компилируем сборку для 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 по умолчанию.