Flutter 1.17 по сравнению с предыдущей стабильной версией принёс больше всего улучшений в плане производительности, одним из ключевых моментов стал внутренний механизм оптимизации Navigator
. В данной статье мы рассмотрим изменения Navigator
от версии OnClickListener 1.12 до 1.17 и узнаем, какие именно улучшения были внедрены в Flutter 1.17.
Одним из самых интересных изменений в версии 1.17 стало то, что после открытия нового непрозрачного экрана старые страницы внутри маршрута больше не вызывают метод build
.
Хотя сам метод build
обычно легковесен, его выполнение при условии, когда это не требуется, противоречит нашим ожиданиям. Оптимизация была реализована в файлах stack.dart
и overlay.dart
.
Изменения в файле stack.dart
заключались в том, чтобы сделать логику RenderStack
общими статическими методами getIntrinsicDimension
и layoutPositionedChild
, тем самым делая часть возможностей размещения Stack
доступной для Overlay
.
Основные изменения в файле overlay.dart
являются ключевыми для этой оптимизации.
На самом деле используемый нами 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);
В самом 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`.

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

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

Затем можно заметить, что метод `build` снова выполняется внутри `Overlay`. - Длина `_entries` всё ещё составляет 4;
- Длина `onstageChildren` становится равной 2, то есть текущее количество видимых `OverlayEntry` составляет 2;
- Длина `offstageChildren` составляет 1, то есть в данный момент имеется один невидимый `OverlayEntry`.
На данном этапе страница B уже открыта полностью, поэтому длина `onstageChildren` восстанавливается до значения 2, что соответствует двум `OverlayEntry`, связанным с страницей B; а страница A, которая стала невидимой, перемещена в `offstageChildren`.
> Почему только один `OverlayEntry` страницы A помещён в `offstageChildren`? Это будет объяснено позже.

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


Таким образом, можно заметить, что каждый раз при открытии новой страницы:
- Сначала два `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` даже тогда, когда невидим;

*Где же устанавливается значение `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
;rebuild
)Это связано с тем, что OverlayEntry
внутри Overlay
преобразуется в _OverlayEntry
для выполнения задач, а GlobalKey
используется внутри _OverlayEntry
. Когда Widget
использует GlobalKey
, его Element
становится глобальным.
При вызове метода inflateWidget
у Element
, если ключ является GlobalKey
, то вызывается метод _retakeInactiveElement
, который возвращает существующий объект Element
, обеспечивая его повторное использование в других местах. В ходе этого процесса Element
удаляется из текущего родителя и добавляется в новый.
Этот процесс вызывает метод update
у объекта Element
, а сам _OverlayEntry
является виджетом типа StatefulWidget
. Поэтому метод update
соответствующего StatefulElement
приведёт к перестроению (rebuild
).
Чтобы избежать перестроения старой страницы при каждом открытии страницы в версии 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, что подтверждает логику открытия и закрытия страниц.
По результатам можно сказать, что эти изменения действительно привели к значительному повышению производительности. Однако стоит отметить, что это улучшение применимо только к непрозрачным страницам. Для прозрачных страниц, таких как PopModal
, всё же требуется выполнение rebuild
.
Metal
— это нижнеуровневый графический интерфейс программирования для iOS, аналогичный OpenGL ES
, который позволяет непосредственно взаимодействовать с GPU через API на устройствах iOS.С версии 1.17 Flutter использует Metal
для рендера на поддерживающих его устройствах iOS, что, согласно официальным данным, увеличивает производительность примерно на 50%. Подробнее см.: https://github.com/flutter/flutter/wiki/Metal-on-iOS-FAQ
На Android благодаря оптимизациям Dart VM размер приложения может уменьшиться примерно на 18.5%.
Версия 1.17 также внесла оптимизацию в процесс загрузки большого количества изображений, что позволило значительно повысить производительность во время быстрого скроллинга (через задержку очистки контекста IO Thread). Теоретически это должно привести к экономии около 70% памяти по сравнению с базовым уровнем.
Так что основные темы были рассмотрены. В конце хотелось бы "нагло" рекомендовать мою новую книгу «Flutter: практика и подробное руководство», которую недавно выпустил:
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )