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

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

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

О памятных утечках и оптимизации в Flutter & Dart, возможно, это не так сложно, как кажется

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

Давайте начнем с общих понятий. Приложение создает корневой объект, который напрямую или косвенно ссылается на все остальные объекты, созданные приложением. Обычно можно представить эту связь как цепочку объектов, где если какой-то звеньев этой цепочки отсутствует, то объекты, которые больше не имеют ссылок, будут освобождены:

root -> A -> B -> C
root -> A -> B -/- C (Указывает сборщику мусора на освобождение памяти C)

Более наглядный пример можно найти в документации Flutter:

class Child {}

class Parent {
  Child? child;
}

Parent parent1 = Parent();

void myFunction() {

  Child? child = Child();

  // Объект `child` был выделен в памяти.
  // Он теперь удерживается от сборки мусора одним путём удержания (root ... -> myFunction -> child).

  Parent? parent2 = Parent()..child = child;
  parent1.child = child;

  // В этот момент объект `child` имеет три пути удержания:
  // root ... -> myFunction -> child
  // root ... -> myFunction -> parent2 -> child
  // root -> parent1 -> child

  child = null;
  parent1.child = null;
  parent2 = null;

  // В этот момент экземпляр `child` становится недостижимым
  // и будет в конечном итоге собран мусором.

  ...
}
```Когда мы говорим о памятных утечках, обычно имеется в виду ситуация, когда **неиспользуемые объекты остаются занятыми программой и не могут быть освобождены**, что может привести к утечкам памяти. Сборщик мусора Dart не может предотвратить все возможные утечки памяти, поскольку он может освободить только те объекты, которые больше не имеют ссылок.

> Обычно, когда объекты создаются с помощью конструкторов, связанная с ними память выделяется в куче Dart VM (виртуальной машиной). Dart VM отвечает за выделение памяти для объектов при их создании и её освобождение при прекращении использования этих объектов.

В Dart, если объекты, которые больше не нужны, всё ещё существуют благодаря некоторым ссылкам, таким как глобальные или статические переменные, сборщик мусора не сможет их распознать, что приведёт к утечкам памяти. Часто встречающиеся ситуации включают:

- Удерживание глобальными/статическими переменными
- Удерживание в замыканиях
- Неправильное использование метода dispose
- ...**Что касается этих сценариев, большинство утечек связано с `BuildContext`**. Почему `BuildContext` легко утекает? Это происходит потому что многие операции связаны с `BuildContext`, такие как: `Theme.of(context)`, `Provider.of(context)`, `context.read()`, `context.pop()` и так далее.**Когда `BuildContext` утекает, это обычно означает, что весь компонент или страница становится недоступной для сборки мусора**. Поскольку `BuildContext` является абстракцией от `Element`, а `Element` служит "мостом" между `Widget` и `RenderObject`.Поскольку в `Element` содержится сильная ссылка на `Widget` и `RenderObject`, сборка мусора этих двух объектов зависит от того, был ли вызван метод `unmount` у `Element`. Даже удаление состояния `StatefulWidget` также зависит от его `Element.unmount`:

![](http://img.cdn.guoshuyu.cn/2bkj9/image1.png)

> Так как `Element` равен `BuildContext`, утечка `BuildContext` эквивалентна тому, что все остальные объекты становятся недоступными для сборки мусора.

Таким образом, мы понимаем, что для того чтобы объекты могли быть собраны сборщиком мусора, они должны быть освобождены от других объектов, то есть **проще говоря, присвоить null тем объектам, которые должны быть собраны мусором**. В действительности во многих действиях `dispose` в Flutter/Dart соответствующие слушатели устанавливаются в null:

![](http://img.cdn.guoshuyu.cn/2bkj9/image2.png)

Но почему я сказал ранее, что утечки в Flutter на уровне Dart не являются "полностью" утечками? **Потому что возможность утечки не равна определенной утечке, а временная утечка не равна полной утечке**.

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

```dart
Timer(Duration(seconds: 5), () {
  print(context.size); // Удерживает ссылку на контекст
});
```Однако это не является «смертельной утечкой», поскольку для данного замыкания его жизненный цикл составляет всего 5 секунд, после чего это замыкание больше не будет иметь внешних ссылок, и сборщик мусора сможет успешно очистить его.

Аналогичный пример: в следующем коде `Future.delayed` удерживает `context`, поэтому сразу после выполнения `Navigator.pop` страница, содержащая этот контекст, не может быть собрана мусором. Однако, как показано на рисунке, если ждать 5 секунд и затем запустить сборку мусора вручную, количество экземпляров в DevTools уменьшится, и конечная страница всё же будет успешно собрана мусором:

```dart
ElevatedButton(
    onPressed: () {
      Future.delayed(Duration(seconds: 5), () {
        print(this.context.widget);
      });
      Navigator.pop(context);
    },
    child: Text("назад")),

Похожий код представлен ниже, где handler захватывает context в замыкании благодаря Theme.of, но из-за того что срок жизни замыкания не превышает срок жизни виджета, это не вызывает проблем с сборкой мусора:

@override
Widget build(BuildContext context) {
  final handler = () => print(Theme.of(context));

  return ElevatedButton(
    onPressed: handler,
    child: Text('Применить тему'),
  );
}

Далее рассмотрим ещё один пример, где мы открываем таймер Timer на каждом экране, а при выходе с экрана не отключаем его. В результате страницы не могут быть удалены, так как callback таймера всё ещё используется другими объектами внутри движка:```dart int _counter = 0; Timer? _timer; @override void initState() { _timer = Timer.periodic(Duration(seconds: 1), (timer) { print(this._counter); print(timer.tick); }); super.initState(); }


![](http://img.cdn.guoshuyu.cn/20241221_DL/image4.gif)

Однако, если вы измените код таким образом, чтобы `callback` таймера захватывал `context`:

```dart
int _counter = 0;
Timer? _timer;
@override
void initState() {
  _timer = Timer.periodic(Duration(seconds: 1), (timer) {
    print(context.size);
  });
  super.initState();
}

На самом деле возникнет ошибка типа «Этот виджет был демонтирован, поэтому состояние больше не имеет контекста (и следует считаться недействительным)». Однако после этого страница может быть удалена сборщиком мусора, поскольку аномалия в callback прерывает последующие запланированные события таймера.

Таким образом, относительно Timer, хотя мы не освобождаем его, если callback таймера сталкивается с ошибками, цикл будет прерван, и замыкание станет доступным для сборки мусора.

Если вас интересует механизм работы Timer, можно посмотреть здесь: https://juejin.cn/post/7383281753145475099#heading-5 . Также стоит отметить связь между асинхронными Future и Timer. Еще один пример — это AnimationController, как показано ниже:

int _counter = 0;
late AnimationController animationController;

@override
void initState() {
  animationController =
      AnimationController(vsync: this, duration: const Duration(seconds: 60));
  animationController.addListener(() {
    print(this._counter);
  });
  animationController.repeat();
  super.initState();
}

imageКонечно, проблема с AnimationController чаще всего связана с тем, что глобальный singleton SchedulerBinding.instance.scheduleFrameCallback во время выполнения (tick) удерживает замыкание.Итак, типичный пример утечки памяти через глобальное замыкание представлен ниже. Когда test добавляется в глобальный список closures, это приводит к тому, что замыкание постоянно удерживается внешними объектами, что делает его недоступным для сборки мусора, а также контекст и состояние, захватываемые этим замыканием, остаются недоступными для удаления:

final List<Function> closures = [];

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  void initState() {
    var test = () {
      _counter++;
      print(context.widget);
    };

    closures.add(test);

    super.initState();
  }
}

Похожий типичный пример можно найти в официальном исправлении проблемы утечки памяти в UndoHistory. Переменная UndoManager.client является статической переменной, которая должна освобождать свои ссылки при потере фокуса и уничтожении контрола; в противном случае статическая переменная будет продолжать удерживать UndoHistoryState, что приведет к утечке памяти:

image

UndoHistory является внутренним компонентом TextField.

Так что, возможно, вы ожидали чего-то другого? На самом деле, небольшая ошибка при создании замыкания может привести к утечке памяти, но обычно эти замыкания всё ещё могут быть собраны сборщиком мусора. Основная причина заключается в том, насколько серьёзна утечка памяти, если нет статических и глобальных ссылок, то обычно срок жизни замыкания не слишком велик, и они всё же будут собраны сборщиком мусора в конце концов.Конечно, хорошие практики программирования могут ускорить процесс сборки мусора и одновременно предотвратить возникновение утечек памяти. Поэтому правильное использование контекста действительно важно, например:

  • Избегайте помещения контекста в асинхронные операции и замыкания,
  • Избегайте того, чтобы замыкания были глобально удерживаемыми. Краткое содержание: Будьте осторожны со статическими и глобальными переменными, обращайте внимание на BuildContext и захват замыканий.

Конечно, иногда память может утечь из-за проблем более низкого уровня, например, в прошлом было недопустимое захватывание замыканий в асинхронных операциях #42457, что привело к проблемам утечки памяти:

Кроме того, важно избегать увеличения потребления памяти, например, при работе с большими объемами данных, использовать BytesBuilder вместо Uint8List, чтобы минимизировать частоту сборки мусора.

Наконец, использование WeakReference и Finalizer в подходящих случаях также помогает эффективно оптимизировать работу с памятью.

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