performancememorymobile-qanon-functionalqa

Утечки памяти: как поймать баг, который появляется только через час

Самые неприятные баги, которые я ловил, никогда не воспроизводились за пять минут. Тест-кейс пройден, смок зелёный, релиз ушёл. А через неделю в отзывах: «через час игры всё начинает тормозить», «вылетает, если долго листать ленту», «телефон греется, приложение закрывается само». Открываешь приложение, проверяешь — всё летает. Потому что ты открыл его на две минуты, а пользователь сидит в нём сорок.

Почти всегда за такими отзывами стоит утечка памяти. И почти всегда её можно было найти до релиза, если знать, где искать.

Что такое утечка, если смотреть глазами тестировщика

Разработчик скажет: объект больше не нужен, но на него осталась ссылка, и сборщик мусора не может его освободить. Для QA полезнее другое определение: память растёт и не возвращается, когда пользователь повторяет одно и то же действие.

Нормальное приложение «дышит»: открыл экран — память выросла, закрыл — вернулась примерно туда же. Приложение с утечкой поднимается лесенкой: открыл-закрыл, открыл-закрыл, и после каждого круга нижняя точка чуть выше предыдущей. Сто кругов — и система убивает процесс.

Отсюда главное правило: утечку не видно на одном прохождении. Её видно только на повторении.

Как утечка выглядит со стороны пользователя

Симптомы почти никогда не говорят «память» прямым текстом. Вот что обычно попадает в отзывы и в тикеты поддержки:

  • Постепенные тормоза. Чем дольше сессия, тем ниже FPS и тем дольше открываются экраны. Сборщик мусора работает всё чаще и всё дольше.
  • Вылеты без стектрейса. На Android система убивает процесс через Low Memory Killer, на iOS — через jetsam. Приложение просто исчезает, и в Crashlytics часто нет нормального краша, потому что это не исключение, а убийство процесса извне.
  • Сброс состояния после сворачивания. Пользователь свернул приложение на минуту, вернулся — а оно стартует заново. В фоне процесс с раздутой памятью убивают первым, об этом прямо пишет документация Android про жизненный цикл процесса.
  • Нагрев и разряд батареи. Постоянная работа сборщика мусора — это CPU.
  • «На старом телефоне не работает». На флагмане с 12 ГБ утечка проживёт два часа, на бюджетнике с 3 ГБ — двадцать минут. Поэтому жалобы приходят неравномерно и выглядят как «проблема конкретной модели».

Если видишь в отзывах «долго», «через какое-то время», «если не закрывать» — это повод проверить память, даже если никто не сказал слово «утечка».

Сценарии, которые проявляют утечки

Утечки живут в местах, где что-то создаётся и уничтожается много раз. Вот сценарии, которые я прогоняю в первую очередь.

  • Открыть-закрыть экран 20–50 раз подряд. Карточка товара, профиль, настройки, попап магазина. Самый частый источник — экран, который при закрытии не отписался от событий или оставил живой таймер.
  • Повороты экрана. На Android поворот пересоздаёт Activity. Если что-то держит ссылку на старую, после каждого поворота в памяти остаётся целый экран.
  • Бесконечная лента и скролл. Листать ленту с картинками пять минут. Если картинки не выгружаются, память растёт линейно.
  • Свернуть-развернуть. 20 раз уйти в фон и вернуться. Ловит подписки, которые создаются заново при каждом возврате.
  • Повтор игровой сессии. Для игр: уровень → проигрыш → рестарт, 30 раз подряд. Объекты прошлого уровня, которые не уничтожились, копятся.
  • Смена аккаунта и логаут. Данные прошлого пользователя иногда остаются в памяти навсегда, а это заодно и проблема безопасности.
  • Плохая сеть. Запросы, которые отменились по таймауту, но их колбэки продолжают держать экран.
  • Долгая сессия без действий. Просто оставить приложение открытым на час. Ловит утечки в таймерах, анимациях, поллинге и рекламных SDK.

Отдельно про рекламу и SDK: утечки в сторонних библиотеках — обычное дело. Если память растёт только при показе интерстишела, это тоже баг, и его тоже надо репортить, даже если «это не наш код».

Как мерить: быстрый способ и правильный способ

Не нужно быть разработчиком, чтобы увидеть утечку. Нужно уметь снять две цифры: память до и после повторения.

Быстрый способ — без профайлера. На Android хватает adb:

adb shell dumpsys meminfo com.example.app

Смотри на строку TOTAL PSS. Сними её после холодного старта, потом прогони сценарий 30 раз, вернись на тот же экран, подожди 10–20 секунд и сними снова. Если цифра выросла на десятки мегабайт и не опускается — это кандидат на утечку. На iOS похожую грубую картину дают Xcode → Debug Navigator → Memory при запуске с Mac.

Правильный способ — профайлер.

  • Android: Memory Profiler в Android Studio показывает график памяти в реальном времени, умеет снимать heap dump и сам подсвечивает утекшие Activity и Fragment. Если у команды есть debug-сборка, попросите встроить LeakCanary: он сам ловит утечки во время обычного ручного тестирования и показывает уведомление с цепочкой ссылок. Для QA это почти бесплатный детектор.
  • iOS: Xcode Instruments, инструменты Allocations и Leaks, плюс Memory Graph в отладчике. Apple подробно описывает это в гайде про измерение памяти.
  • Unity: встроенный Memory-модуль Profiler и пакет Memory Profiler. Главный приём — снять снапшот в меню, сыграть пять уровней, вернуться в меню, снять второй и сравнить. Всё, что осталось от уровней, — утечка.
  • Веб: Chrome DevTools → Memory, heap snapshot до и после сценария. У Google есть хороший разбор поиска утечек с методикой «трёх снапшотов».

Принцип у всех инструментов одинаковый: снапшот → повторение → снапшот → сравнение. Не нужно понимать весь граф объектов. Достаточно увидеть, что после 30 открытий экрана в памяти живут 30 копий этого экрана.

Как не обмануть себя

Пара ловушек, на которых я сам спотыкался.

  • Рост — ещё не утечка. Кэши картинок, пулы объектов, прогретые шейдеры законно увеличивают память после первых действий. Утечка — это когда рост продолжается при каждом повторении и не упирается в потолок. Поэтому важна не одна цифра, а тренд: 5, 10, 20, 30 повторений.
  • Дай сборщику мусора отработать. Снимай цифру не сразу после действия, а через 10–20 секунд простоя. В профайлерах есть кнопка принудительного GC — жми её перед снапшотом.
  • Тестируй release-сборку. Debug-сборки едят больше памяти и ведут себя иначе. Финальную проверку делай на том, что пойдёт пользователям.
  • Тестируй на слабом устройстве. Утечка, которая на флагмане даёт плюс 200 МБ за час, на бюджетнике убьёт процесс через 15 минут. Для мобильных проектов имеет смысл держать в матрице устройств хотя бы один телефон с 3–4 ГБ памяти.

Что приложить к баг-репорту

Баг «приложение тормозит через час» разработчик может искать неделю. Баг с данными — за час. Вот что я прикладываю:

  • Точный сценарий повторения: «открыть карточку товара и вернуться назад, 30 раз».
  • Цифры: память после старта, после 10, 20 и 30 повторений. Лучше таблицей или скриншотом графика из профайлера, где видна лесенка.
  • Устройство, ОС и тип сборки (release или debug).
  • Heap dump или снапшот, если снял: файл .hprof для Android, .memgraph для iOS, .snap для Unity.
  • Отчёт LeakCanary, если он сработал: там уже есть цепочка ссылок, и это почти готовый ответ.
  • Что не течёт: «тот же сценарий на экране профиля память не растит». Это сильно сужает поиск.

Как встроить это в процесс

Утечки не нужно искать вручную перед каждым релизом. Достаточно трёх привычек:

  • Soak-сценарий в регрессе. Один длинный сценарий на 30–60 минут: основные экраны по кругу. Прогоняется раз в релиз, лучше автоматически, с записью памяти в начале и в конце.
  • Порог в CI для критичных экранов. Если есть UI-автотесты, добавьте шаг: прогнать экран 20 раз и сравнить PSS. Рост больше заданного порога — тест красный.
  • LeakCanary в debug-сборках QA. Самый дешёвый способ: тестировщики находят утечки как побочный эффект обычной работы.

Чек-лист: проверка на утечки памяти

  • Сними память после холодного старта на эталонном экране.
  • Прогони открыть-закрыть ключевые экраны 20–50 раз.
  • Прогони повороты экрана, если приложение их поддерживает.
  • Свернуть-развернуть приложение 20 раз.
  • Для игр: рестарт уровня 30 раз, переход меню → уровень → меню 10 раз.
  • Поскролль ленты и списки с картинками 5 минут.
  • Оставь приложение открытым на 30–60 минут без действий.
  • Проверь показ рекламы и других SDK несколько раз подряд.
  • Каждый раз снимай цифру через 10–20 секунд после действия, лучше после принудительного GC.
  • Смотри на тренд по 5, 10, 20, 30 повторениям, а не на одну цифру.
  • Финальную проверку делай на release-сборке и на слабом устройстве.
  • В баг-репорт: сценарий, цифры по повторениям, устройство, heap dump.

Утечка памяти — это баг, который наказывает самых преданных пользователей: тех, кто сидит в приложении дольше всех. Короткие тест-кейсы его не видят по определению. Видит только тестировщик, который догадался повторить одно и то же действие тридцать раз и посмотреть на цифру.