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

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

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

Flutter 1.17 по сравнению с предыдущей стабильной версией принёс больше всего улучшений в плане производительности, одним из ключевых моментов стал внутренний механизм оптимизации Navigator. В данной статье мы рассмотрим изменения Navigator от версии OnClickListener 1.12 до 1.17 и узнаем, какие именно улучшения были внедрены в Flutter 1.17.

1. Какие улучшения были сделаны в Navigator?

Одним из самых интересных изменений в версии 1.17 стало то, что после открытия нового непрозрачного экрана старые страницы внутри маршрута больше не вызывают метод build.

Хотя сам метод build обычно легковесен, его выполнение при условии, когда это не требуется, противоречит нашим ожиданиям. Оптимизация была реализована в файлах stack.dart и overlay.dart.

  • Изменения в файле stack.dart заключались в том, чтобы сделать логику RenderStack общими статическими методами getIntrinsicDimension и layoutPositionedChild, тем самым делая часть возможностей размещения Stack доступной для Overlay.

  • Основные изменения в файле overlay.dart являются ключевыми для этой оптимизации.

2. Навигация через Overlay

На самом деле используемый нами Navigator является StatefulWidget, а такие методы как pop и push имеют свою логику в классе NavigatorState.

Основная задача NavigatorState — использовать Overlay для управления маршрутами, поэтому основная логика управления страницами находится в Overlay.### 2.1 Что такое Overlay?

Overlay вы, возможно, уже использовали, он позволяет добавлять глобальные плавающие компоненты к MaterialApp в Flutter. Это потому что Overlay представляет собой слоистый контроллер, но управление внутренними компонентами осуществляется через OverlayEntry.

Например, можно использовать overlayState.insert() для добавления OverlayEntry, который создаст новый слой. Метод builder внутри OverlayEntry будет вызван при отображении, обеспечивая нужный эффект размещения.

var overlayState = Overlay.of(context);
var _overlayEntry = new OverlayEntry(builder: (context) {
  return new Material(
    color: Colors.transparent,
    child: Container(
      child: Text(
        '${widget.platform} ${widget.deviceInfo} ${widget.language} ${widget.version}',
        style: TextStyle(color: Colors.white, fontSize: 10),
      ),
    ),
  );
});
overlayState.insert(_overlayEntry);

2.2 Как реализуется навигация с помощью Overlay?

В самом Navigator используется Overlay для управления страницами. Каждый открытый Route по умолчанию вставляет два OverlayEntry в Overlay.

Почему именно два — это будет объяснено позже.

Внутри Overlay, логика отображения списка _entries осуществляется через _Theatre. В _Theatre есть два параметра: onstage и offstage.

  • onstage представляет собой Stack, который отображает onstageChildren.reversed.toList(growable: false), то есть видимую часть;
  • offstage отображает список offstageChildren, то есть невидимую часть;
return _Theatre(
  onstage: Stack(
    fit: StackFit.expand,
    children: onstageChildren.reversed.toList(growable: false),
  ),
  offstage: offstageChildren,
);
```Проще говоря, если у нас есть три страницы [A, B, C], то:

- C должна находиться на `onstage`;
- A и B должны находиться на `offstage`.

Конечно, все эти страницы A, B и C вставляются в `Overlay` как `OverlayEntry`. При этом каждая страница по умолчанию имеет два `OverlayEntry`, поэтому для [A, B, C] должно быть шесть `OverlayEntry`.

Например, после запуска приложения мы видим страницу A. В этот момент в `Overlay`:

- Длина `_entries` равна 2, то есть всего два `OverlayEntry` в списке;
- Длина `onstageChildren` равна 2, то есть два текущих видимых `OverlayEntry`;
- Длина `offstageChildren` равна 0, то есть нет невидимых `OverlayEntry`.

![image](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image1)

При переходе на страницу B, в `Overlay`:

- Длина `_entries` увеличивается до 4, то есть добавлено еще два `OverlayEntry`;
- Длина `onstageChildren` также составляет 4, то есть четыре текущих видимых `OverlayEntry`;
- Длина `offstageChildren` остается равной 0, то есть пока нет невидимых `OverlayEntry`.

![image](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image2)

На данном этапе `Overlay` находится в состоянии открытия страницы, то есть страница A все еще видима, а страница B находится в процессе анимированного открытия.

![image](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image3)

Затем можно заметить, что метод `build` снова выполняется внутри `Overlay`. - Длина `_entries` всё ещё составляет 4;
- Длина `onstageChildren` становится равной 2, то есть текущее количество видимых `OverlayEntry` составляет 2;
- Длина `offstageChildren` составляет 1, то есть в данный момент имеется один невидимый `OverlayEntry`.![](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image4)

На данном этапе страница B уже открыта полностью, поэтому длина `onstageChildren` восстанавливается до значения 2, что соответствует двум `OverlayEntry`, связанным с страницей B; а страница A, которая стала невидимой, перемещена в `offstageChildren`.

> Почему только один `OverlayEntry` страницы A помещён в `offstageChildren`? Это будет объяснено позже.

![](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image5)

Затем, как показано на следующем рисунке, при открытии страницы C можно заметить аналогичный процесс:

- Длина `_entries` увеличивается до 6;
- Длина `onstageChildren` сначала составляет 4, а затем снова становится равной 2, так как при открытии участвуют две страницы  B и C, а после завершения открытия остаётся только одна страница C;
- Длина `offstageChildren` сначала составляет 1, а затем увеличивается до 2, поскольку сначала невидимой является только страница A, а затем и страницы A и B;

![](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image6)

![](http://img.cdn.guoshuyu.cn/20200608_Flutter-nav+1_17/image7)

Таким образом, можно заметить, что каждый раз при открытии новой страницы:

- Сначала два `OverlayEntry` добавляются в `_entries`;
- Затем происходит состояние открытия страницы, когда длина `onstageChildren` составляет 4;
- В конце состояние открытия страницы завершается, когда длина `onstageChildren` составляет 2, а нижняя страница, которая стала невидимой, добавляется в `offstageChildren`.### 2.3. Overlay и Route

*Почему каждый раз в `_entries` добавляются два `OverlayEntry`?*

Это связано с `Route`. Например, при использовании `MaterialPageRoute` для открытия нового экрана с помощью `Navigator` создаются два `OverlayEntry`, причём это происходит в одном из базовых классов `Route`, таких как `ModalRoute`.

В методе `createOverlayEntries` класса `ModalRoute` через `_buildModalBarrier` и `_buildModalScope` создаются два `OverlayEntry`, где:

- `_buildModalBarrier` обычно создаёт макет;
- `_buildModalScope` создаёт `OverlayEntry`, который служит контейнером для страницы;

**Поэтому при открытии одной страницы всегда существуют два `OverlayEntry`: один для макета и другой для самой страницы**.

```dart
  @override
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }

Почему количество вставляемых в offstageChildren увеличивается на 1, а не на 2 при наличии двух OverlayEntry?

Логически говоря, если взять за основу пример с тремя страницами [A, B, C], в _entries должно быть 6 OverlayEntry. Однако, когда страницы B и C становятся невидимыми, нет необходимости хранить их макеты, так как это приведёт к ненужной нагрузке.

С точки зрения кода, во время обратного цикла for в _entries:

  • Когда встречается entry.opaque == true, все последующие OverlayEntry не попадают в onstageChildren;
  • В offstageChildren добавляются только те OverlayEntry, где entry.maintainState == true.```dart @override Widget build(BuildContext context) { final List onstageChildren = []; final List offstageChildren = []; bool onstage = true; for (int i = _entries.length - 1; i >= 0; i -= 1) { final OverlayEntry entry = _entries[i]; if (onstage) { onstageChildren.add(_OverlayEntry(entry)); if (entry.opaque) onstage = false; } else if (entry.maintainState) { offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry))); } } return _Theatre( onstage: Stack( fit: StackFit.expand, children: onstageChildren.reversed.toList(growable: false), ), offstage: offstageChildren, ); }

Внутри `OverlayEntry`:

- `opaque` указывает, является ли `OverlayEntry` "непрозрачным" (то есть полностью закрывающим `Overlay`);
- `maintainState` указывает, что этот `OverlayEntry` должен быть добавлен в `_Theatre`.

Поэтому можно заметить, что после полного открытия страницы, первые два `OverlayEntry`:

- У `overlayEntry` (макета) `opaque` устанавливается в `true`, чтобы следующий `OverlayEntry` не попал в `onstageChildren`, то есть не был отображен;
- У `pageEntry` (страницы) `maintainState` устанавливается в `true`, чтобы он попал в `offstageChildren` даже тогда, когда невидим;

![Иллюстрация](http://img.cdn.guoshuyu.cn/2bk20200608_Flutter-nav+1_17/image8)

*Где же устанавливается значение `opaque`?*

Процесс установки значения `opaque` представлен ниже. В другом базовом классе `TransitionRoute` для `MaterialPageRoute` видно, что изначально значение `opaque` для макета устанавливается в `false`, а затем в методе `completed` устанавливается в `true`. Аргумент `opaque` в `PageRoute` определяется как `@override bool get opaque => true;`.

Пример кода:```dart
// Пример установки opaque
bool _opaque = false;

void setOpaque(bool value) {
  _opaque = value;
}

// Пример использования
setOpaque(true);
```В `PopupRoute` значение `opaque` равно `false`, так как `PopupRoute` обычно имеет прозрачный фон и требует совмещения с предыдущим экраном.

```dart
void _handleStatusChanged(AnimationStatus status) {
    switch (status) {
      case AnimationStatus.completed:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
      case AnimationStatus.dismissed:
        if (!isActive) {
          navigator.finalizeRoute(this);
          assert(overlayEntries.isEmpty);
        }
        break;
    }
    changedInternalState();
}

На данном этапе мы прояснили логику работы Overlay при открытии страницы:

  • При каждом открытии страницы в Overlay добавляются два OverlayEntry;
  • В процессе открытия количество onstageChildren составляет 4, так как два экрана совмещаются;
  • После завершения открытия количество onstageChildren становится равным 2, поскольку значение opaque макета устанавливается в true, что делает последующий экран невидимым;
  • OverlayEntry, имеющий maintainState равным true, переходит в состояние offstageChildren после того, как становится невидимым;

Дополнительно стоит отметить, что позиция, куда маршрут будет вставлен, зависит от OverlayEntry, переданного методом route.install. Например, метод push передаёт _history (стек маршрутов страниц). ```## 3. Новый подход в версии 1.17 для Overlay

Почему до версии 1.17 при открытии нового экрана старый экран выполнял метод build? Здесь есть две основные причины:

  • Каждому OverlayEntry присваивается уникальный GlobalKey<_OverlayEntryState>;
  • Процесс OverlayEntry включает переход от состояния onstage к состоянию offstage внутри _Theatre;

3.1 Почему происходит перестроение (rebuild)

Это связано с тем, что OverlayEntry внутри Overlay преобразуется в _OverlayEntry для выполнения задач, а GlobalKey используется внутри _OverlayEntry. Когда Widget использует GlobalKey, его Element становится глобальным.

При вызове метода inflateWidget у Element, если ключ является GlobalKey, то вызывается метод _retakeInactiveElement, который возвращает существующий объект Element, обеспечивая его повторное использование в других местах. В ходе этого процесса Element удаляется из текущего родителя и добавляется в новый.


Этот процесс вызывает метод update у объекта Element, а сам _OverlayEntry является виджетом типа StatefulWidget. Поэтому метод update соответствующего StatefulElement приведёт к перестроению (rebuild).

3.2 Почему версия 1.17 не перестраивает страницу

Чтобы избежать перестроения старой страницы при каждом открытии страницы в версии 1.17, здесь отключены методы onstage и offstage у _Theatre, заменив их на параметры skipCount и children.Кроме того, _Theatre был преобразован с RenderObjectWidget до MultiChildRenderObjectWidget, и затем в _RenderTheatre была использована общая способность размещения из RenderStack.

  @override
  Widget build(BuildContext context) {
    // Этот список заполняется справа налево, а затем переворачивается ниже перед добавлением его в дерево.
    final List<Widget> children = <Widget>[];
    bool onstage = true;
    int onstageCount = 0;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[i];
      if (onstage) {
        onstageCount += 1;
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
        ));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
          tickerEnabled: false,
        ));
      }
    }
    return _Theatre(
      skipCount: children.length - onstageCount,
      children: children.reversed.toList(growable: false),
    );
  }

Теперь все _entries в Overlay помещаются в один MultiChildRenderObjectWidget, то есть они находятся в одном Element, а не переключаются между onstage стеком и списком offstage.Новый _Theatre объединяет два массива в один массив children, а затем устанавливает skipCount за пределами onstageCount. При размещении получается _firstOnstageChild, который используется для размещения. Когда меняются children, это вызывает метод insertChildRenderObject у MultiChildRenderObjectElement, что не влияет на предыдущую страницу, поэтому она не перестраивается.

RenderBox get _firstOnstageChild {
    if (skipCount == super.childCount) {
        return null;
    }
    RenderBox child = super.firstChild;
    for (int skip = skipCount; skip > 0; skip--) {
        final StackParentData childParentData = child.parentData as StackParentData;
        child = childParentData.nextSibling;
        assert(child != null);
    }
    return child;
}
RenderBox get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;

После открытия страницы количество children действительно меняется с 4 до 3, а значение onstageCount также уменьшается с 4 до 2, что подтверждает логику открытия и закрытия страниц. image image

По результатам можно сказать, что эти изменения действительно привели к значительному повышению производительности. Однако стоит отметить, что это улучшение применимо только к непрозрачным страницам. Для прозрачных страниц, таких как PopModal, всё же требуется выполнение rebuild. image

Четвертый раздел: Другие оптимизацииMetal — это нижнеуровневый графический интерфейс программирования для iOS, аналогичный OpenGL ES, который позволяет непосредственно взаимодействовать с GPU через API на устройствах iOS.

С версии 1.17 Flutter использует Metal для рендера на поддерживающих его устройствах iOS, что, согласно официальным данным, увеличивает производительность примерно на 50%. Подробнее см.: https://github.com/flutter/flutter/wiki/Metal-on-iOS-FAQ image На Android благодаря оптимизациям Dart VM размер приложения может уменьшиться примерно на 18.5%. Версия 1.17 также внесла оптимизацию в процесс загрузки большого количества изображений, что позволило значительно повысить производительность во время быстрого скроллинга (через задержку очистки контекста IO Thread). Теоретически это должно привести к экономии около 70% памяти по сравнению с базовым уровнем. image Так что основные темы были рассмотрены. В конце хотелось бы "нагло" рекомендовать мою новую книгу «Flutter: практика и подробное руководство», которую недавно выпустил:

Опубликовать ( 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