Причины возникновения утечек памяти и как их избежать
Flutter скрывает от разработчика ручное управление памятью. Кажется, что сборщик мусора все сделает сам. Но на практике утечки случаются регулярно. Экран закрыт, а какие-то объекты продолжают висеть в памяти. После нескольких переходов туда-сюда приложение начинает тормозить, а на слабых устройствах - вылетать. Это не баг фреймворка, это ошибки в коде, которые можно и нужно контролировать.
Откуда берутся утечки:
Чаще всего проблема в том, что объект создается, но не освобождается. Экран ушел, а контроллеры, подписки, таймеры или слушатели продолжают жить. Они держат ссылки на другие объекты, а те - на виджеты. В итоге память не отдается системе.
Контроллеры и dispose:
TextEditingController, ScrollController, AnimationController, FocusNode - все это нужно уничтожать в dispose(). Если этого не делать, каждый открытый экран будет оставлять после себя мусор. Правило простое: создал в initState - удали в dispose.
Подписки на Stream и слушатели:
StreamSubscription, addListener у ChangeNotifier и ValueNotifier - это классические источники утечек. Если подписался, но не отписался, поток будет слать события в уже мертвый экран. А заодно удерживать его в памяти. Решение: сохранять подписку и отменять ее в dispose.
BLoC и другие сущности с жизненным циклом:
Если BLoC создается прямо внутри State, нужно не забыть вызвать close(). Удобнее использовать BlocProvider, который сам управляет жизненным циклом. Главное правило: кто создал, тот и закрывает.
Таймеры:
Timer.periodic запускает повторяющиеся задачи. Если не отменить таймер при уходе с экрана, он будет продолжать тикать, вызывать setState и держать экран в памяти. Всегда сохраняйте ссылку на таймер и вызывайте cancel() в dispose.
Сохраненный BuildContext:
BuildContext нельзя хранить в сервисах, синглтонах или долгоживущих объектах. Он привязан к дереву виджетов. Хранив его надолго, вы случайно удерживаете в памяти целую часть интерфейса. Лучший способ - передавать контекст только в момент вызова, не сохраняя его.
Кэши без ограничений:
Кэш - это тоже утечка, если он растет бесконтрольно. Изображения, модели, результаты запросов - все это копится в памяти, если вовремя не чистить. Простое решение: ограничить размер кэша или очищать его при выходе пользователя.
Как искать утечки:
Самый прямой способ - Flutter DevTools. Раздел Memory показывает, как меняется потребление памяти. Простой сценарий: открываете экран, закрываете, повторяете несколько раз. Если память не возвращается к исходному уровню - есть проблема.
Можно пойти дальше: сделать снимок памяти до открытия экрана, потом после нескольких открытий/закрытий и сравнить. Если в списке объектов появляются лишние экземпляры TextEditingController, Bloc, State - это явный сигнал.
Leak Tracker:
Для автоматического поиска утечек можно подключить пакет . Он отслеживает объекты с жизненным циклом и выдает предупреждения в консоль, если что-то не было освобождено. Удобно для отладки в процессе разработки.
Важные правила:
Если есть initState, проверьте, есть ли dispose.
Если есть addListener, найдите соответствующий removeListener.
Если есть StreamSubscription, убедитесь, что вызывается cancel.
Если есть Timer, проверьте, что он отменяется.
Если есть кэш, задайте ему ограничения.
После await перед setState проверьте mounted.
Вывод:
Утечки памяти во Flutter - это не магия, а следствие забытых dispose, cancel и removeListener. Они не видны сразу, но накапливаются со временем. Самый надежный способ - профилактика: иметь чек-лист для код-ревью и периодически прогонять подозрительные сценарии через DevTools. Если относиться к dispose как к обязательной части работы с экраном, большинство проблем можно поймать еще до релиза.