1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-N18.md 23 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Быстрое понимание метода toImageSync в Flutter 3.7

С выходом Flutter 3.7 появились два новых метода в пространстве имён dart:ui — Picture.toImageSync и Scene.toImageSync. В отличие от существующих методов Picture.toImage и Scene.toImage, методы с суффиксом Sync являются синхронными, поэтому они не требуют использования ключевого слова await.

Вызов метода toImageSync сразу возвращает объект типа Image, а процесс растрирования происходит асинхронно в фоновом режиме внутри движка.

Введение

Что же такое toImageSync? Почему был создан этот синхронный метод, если уже существует метод toImage?

  • Основной особенностью метода toImageSync является то, что изображение постоянно хранится в памяти GPU, что позволяет значительно увеличить скорость его отрисовки и повторно использовать его, повышая производительность.

  • Метод toImageSync является синхронным, что позволяет компенсировать недостаток асинхронного метода toImage в некоторых случаях.

Иллюстрация

Официальная документация также приводит несколько примеров использования метода toImageSync:

  • Быстрое захватывание дорогостоящего растрового изображения, которое можно многократно использовать между несколькими кадрами;
  • Применение в многослойных фильтрах изображений;
  • Применение в пользовательских шейдерах.На данный момент наиболее очевидное применение метода toImageSync реализовано по умолчанию в Android при использовании анимации перехода страниц ZoomPageTransitionsBuilder. Благодаря особенностям метода toImageSync производительность анимации перехода страниц на платформе Android была увеличена почти вдвое, что позволило снизить количество пропущенных кадров и повысить частоту обновления экрана.> Однако стоит отметить, что это достигнуто за счёт отказа от некоторых других возможностей, которые мы рассмотрим далее.

SnapshotWidget

Ранее было упомянуто, что метод toImageSync значительно улучшил производительность анимации перехода страниц по умолчанию на Android. Но как именно это было сделано? Для ответа на этот вопрос следует обратиться к новому виджету Flutter 3.7 — SnapshotWidget.

Начальный вариант виджета назывался RasterWidget, но в конечном итоге он был упрощён до SnapshotWidget, так как последний лучше всего соответствует его целям.

Иллюстрация

Концепция

SnapshotWidget предназначен для преобразования потомков в снимок (ui.Image) и замены ими, другими словами, все потомки превращаются в одно изображение-снимок. Получение снимка осуществляется методом Scene.toImageSync.

Теперь вы должны понять, почему toImageSync повышает производительность анимации перехода страниц на Android? Это происходит потому что SnapshotWidget преобразует потомков в снимок при переходе между страницами, а полученные изображения могут повторно использоваться во многих кадрах.

Итак, вопрос заключается в том, какие побочные эффекты вызывает использование SnapshotWidget, который преобразует потомков в снимок (ui.Image) для повышения производительности?Ответ заключается в том, что это влияет на анимацию. Потому что все потомки становятся снимками; если они имеют анимационные эффекты, то эти эффекты будут "заморожены" , более наглядный пример представлен ниже:| FadeUpwardsPageTransitionsBuilder | ZoomPageTransitionsBuilder | | ------------------------------------------------------------ | ------------------------------------------------------------ | | | |

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

Из-за короткого времени выполнения анимации, её скорость может быть глобально снижена путём установки timeDilation = 40.0; и вызова SchedulerBinding.resetEpoch(). Также можно настроить pageTransitionsTheme в теме ThemeData компонента MaterialApp для изменения эффекта перехода страниц.Таким образом, согласно официальной документации, SnapshotWidget используется для помощи в реализации краткосрочных анимационных эффектов, таких как масштабирование, наклон или размытие. Эти анимации могут быть дорогостоящими при сложной структуре потомков, но использование toImageSync позволяет использовать буферизированное изображение. Для некоторых коротких анимаций, таких как переходы страниц с помощью ZoomPageTransitionsBuilder, компонент SnapshotWidget преобразует всех потомков страницы в снимки (ui.Image). Хотя это может привести к "заморозке" анимации потомков при смене страниц, время самого перехода между страницами очень мало, поэтому никаких аномалий заметно не будет, в то же время плавность переходной анимации становится очевидной. Для примера ниже показан код, который после запуска отображает вращающийся логотип, случайно перемещающийся по экрану. В этом примере используются AnimatedSlide и AnimatedRotation, чтобы выполнить анимацию перемещения и вращения соответственно.```dart Timer.periodic(const Duration(seconds: 2), (timer) { final random = Random(); x = random.nextInt(6) - 3; y = random.nextInt(6) - 3; r = random.nextDouble() * 2 * pi; setState(() {}); });

AnimatedSlide( offset: Offset(x.floorToDouble(), y.floorToDouble()), duration: Duration(milliseconds: 1500), curve: Curves.easeInOut, child: AnimatedRotation( turns: r, duration: Duration(milliseconds: 1500), child: Image.asset( 'static/test_logo.png', width: 100, height: 100, ), ), )


![Анимация](http://img.cdn.guoshuyu.cn/20230207_sync/image5.gif)

Если теперь добавить `SnapshotWidget` поверх `AnimatedRotation` и активировать `allowSnapshotting`, можно заметить, что логотип больше не вращается, так как весь его потомок уже преобразован в снимок (`ui.Image`).

| ![Без SnapshotWidget](http://img.cdn.guoshuyu.cn/20230207_sync/image6.png) | ![С SnapshotWidget](http://img.cdn.guoshuyu.cn/20230207_sync/image7.gif) |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------ |

Поэтому `SnapshotWidget` не подходит для мест, где потомкам требуется продолжение анимации или реакция на взаимодействие пользователя, например, для слайдера.

## Использование

Как видно из предыдущего кода, использование `SnapshotWidget` также довольно просто — вам нужно лишь настроить `SnapshotController` и контролировать через `allowSnapshotting`, будет ли потомок рендериться как снимок.

```dart
controller.allowSnapshotting = true;

При захвате снимка SnapshotWidget создаёт новый OffsetLayer и PaintingContext, затем использует super.paint для захвата содержимого (это одна из причин, почему он не поддерживает PlatformView). После этого с помощью toImageSync получается полный снимок (ui.Image) данных, которые передаются SnapshotPainter для рисования.| Контейнер | Снимок | | ------------------------------------------------------------------- | ------------------------------------------------------------ |

Таким образом, для рисования снимка SnapshotWidget требуется SnapshotPainter. По умолчанию это реализуется с помощью встроенного _DefaultSnapshotPainter, но вы можете создать свой собственный SnapshotPainter, чтобы реализовать специфическую логику. SnapshotPainter используется для отрисовки интерфейса снимков вложенных компонентов. Как показано выше, он выбирает между вызовом методов paint и paintSnapshot, в зависимости от того, поддерживает ли дочерний компонент захват (_childRaster == null).

Кроме того, в настоящее время из-за ограничений в реализации метода toImageSync, компонент SnapshotWidget не может захватывать вложенные компоненты типа PlatformView. В случае встречи с PlatformView, поведение SnapshotWidget зависит от значения свойства SnapshotMode:

режим описание
normal По умолчанию выбрасывает ошибку при попытке захвата незахватываемого вложенного компонента
permissive При встрече с незахватываемым вложенным компонентом использует его без захвата
forced Пропускает незахватываемый вложенный компонент

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

Поэтому в SnapshotPainter основное внимание уделяется реализации двух методов — paint и paintSnapshot.

  • Метод paintSnapshot вызывается при отрисовке снимка вложенного компонента.

  • Метод paint использует callback painter (соответствующий super.paint) для отрисовки вложенного компонента. Этот метод будет вызван, если снимок недоступен или если в режиме permissive встретился PlatformView.

Пример

Например, как показано ниже, в методе paintSnapshot можно добавить прозрачность к маленькому логотипу, используя параметр Paint..color:

class TestPainter extends SnapshotPainter {
  final Animation<double> animation;

  TestPainter({required this.animation});

  @override
  void paint(PaintingContext context, ui.Offset offset, Size size,
      PaintingContextCallback painter) {}

  @override
  void paintSnapshot(PaintingContext context, Offset offset, Size size,
      ui.Image image, Size sourceSize, double pixelRatio) {
    final Rect src = Rect.fromLTWH(
        0, 0, sourceSize.width, sourceSize.height);
    final Rect dst = Rect.fromLTWH(
        offset.dx, offset.dy, size.width, size.height);
    final Paint paint = Paint()
      ..color = Color.fromRGBO(0, 0, 0, animation.value)
      ..filterQuality = FilterQuality.low;
    context.canvas.drawImageRect(image, src, dst, paint);
  }

  @override
  void dispose() {
    super.dispose();
  }
}
```  @override
  bool shouldRepaint(covariant TestPainter oldDelegate) {
    return oldDelegate.animation.value != animation.value;
  }
}

На самом деле можно переместить анимацию движения в метод paintSnapshot, а затем управлять состоянием анимации через вызов notifyListeners для прямого обновления отрисовки снимка. Это обеспечивает лучшую производительность, как это реализовано в Android в ZoomPageTransitionsBuilder.

  animation.addListener(notifyListeners);
  animation.addStatusListener(_onStatusChange);

  void _onStatusChange(_) {
    notifyListeners();
  }

  @override
  void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) {
    _drawMove(context, offset, size);
  }

  @override
  void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
    switch (animation.status) {
      case AnimationStatus.completed:
      case AnimationStatus.dismissed:
        return painter(context, offset);
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
    }
    ...
  }

Для более подробной информации обратитесь к реализации системы ZoomPageTransitionsBuilder.

Расширенное исследование

Кроме SnapshotWidget, RepaintBoundary также поддерживает toImageSync. Поскольку toImageSync получает постоянные данные из GPU, то теоретически при реализации сценария отрисовки скриншотов и выделения областей, должна быть достигнута лучшая производительность.

final RenderRepaintBoundary boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final ui.Image image = boundary.toImageSync();
```Кроме того, объекты `Scene` и `_Image` в модуле `dart:ui` являются `NativeFieldWrapperClass1`. Мы уже объясняли, что **`NativeFieldWrapperClass1` имеет логику, которая различается между платформами благодаря движку**.| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image12.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image13.png) |
| --------------------------------------------------------- | --------------------------------------------------------- |

Поэтому если вы установите точку останова в `flutter/bin/cache/pkg/sky_engine/lib/ui/compositing.dart` для метода `toImageSync`, то он может не сработать, так как его реальная реализация находится в платформозависимой части движка.

![](http://img.cdn.guoshuyu.cn/20230207_sync/image14.png)

Кроме того, мы говорили ранее, что `toImageSync` отличается от `toImage` тем, что он постоянно присутствует в памяти GPU. Но где же заключаются различия между ними? Из приведённой выше диаграммы можно заметить:

- `toImageSync` выполняет `Scene:RasterizeToImage` и возвращает обработчик `Dart_Null`
- `toImage` выполняет `Picture:RasterizeLayerTreeToImage` и сразу возвращает результат

Проще говоря:

- `toImageSync` в конечном итоге использует `SkImage::MakeFromTexture` для получения `GPU SkImage` через текстуру
- `toImage` создаёт `SkImage` с помощью `makeImageSnapshot` и `makeRasterImage`. Последний представляет собой операцию копирования изображения в память процессора.

| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image15.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image16.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image17.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image18.png) |
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |

Изначально метод `toImageSync` был назван `toGpuImage`, но затем название было изменено на более универсальное `toImageSync`.![](http://img.cdn.guoshuyu.cn/20230207_sync/image19.png)

Разработка функциональности, связанной с `toImageSync`, также прошла долгий период обсуждений. Вопрос о том, следует ли предоставлять такой API и как его внедрять, был очень сложным, не менее трудным, чем [фоновый изолятор](https://juejin.cn/post/7195825738472620087). Например, были рассмотрены вопросы о необходимости определения сценариев ошибок, обработки этих ошибок на уровне фреймворка, а также о целесообразности использования такого API для повышения производительности.

| ![](http://img.cdn.guoshuyu.cn/20230207_sync/image20.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image21.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image22.png) | ![](http://img.cdn.guoshuyu.cn/20230207_sync/image23.png) |
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |

А `toImageSync` и связанные с ним функции были реализованы благодаря одному очень важному аспекту, который, по моему мнению, заключается в следующем:

>`toGoulmage` предоставляет фреймворку возможность самостоятельно контролировать производительность, что важно, учитывая, что наши приоритеты не всегда совпадают.

# В заключение

`toImageSync`  это всего лишь простой API, но за его созданием стоит множество историй. Одновременно `toImageSync` и его обёртка `SnapshotWidget` имеют своей конечной целью повышение производительности работы с Flutter.Может быть, в данный момент `toImageSync` вам не требуется, а `SnapshotWidget` кажется лишним, но как только вы столкнетесь с сложными сценариями отрисовки, `toImageSync` станет вашим неотъемлемым инструментом.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master