objc-retain.com

Утечки памяти в iOS-приложениях: поиск с помощью Instruments

Утечки памяти в iOS-приложениях: поиск с помощью Instruments

Утечки памяти в iOS-приложениях редко проявляются сразу. Чаще они тихо накапливают объекты, замедляют интерфейс, увеличивают расход батареи и в итоге превращают стабильное приложение в источник жалоб пользователей. Самый практичный способ найти проблему — прогонять подозрительный сценарий под Instruments и сравнивать поведение памяти до и после повторяющихся действий.

За годы работы с Objective-C и ARC-миграциями я убедился: Instruments — это не просто тулза для отладки, а инструмент расследования, который требует системного подхода. Особенно когда речь идёт о приложениях для рынка недвижимости, где один экран карточки объекта может тянуть за собой галерею из 20 изображений, карту с аннотациями, видео-превью и JSON-ответ от API с планировками. В таких условиях даже небольшой цикл удержания быстро превращается в критическую проблему.

Что такое утечка памяти в iOS

Утечка памяти — это ситуация, когда объект больше не нужен приложению, но по-прежнему остаётся в памяти и не освобождается. На iOS это часто связано с сильными ссылками, замыканиями, делегатами, таймерами, наблюдателями и неочевидными удержаниями через контейнеры.

Важно не путать утечку с обычным ростом памяти. Если экран открылся, загрузил изображения, данные и потом память частично не вернулась — это ещё не всегда leak. Иногда это кэш, буферы, временные аллокации или объекты, которые освободятся позже. Поэтому искать нужно не «любое увеличение», а устойчивый рост после одинаковых сценариев и отсутствие освобождения там, где оно ожидается.

На практике я часто сталкиваюсь с ситуацией: команда риэлторского приложения жалуется, что каталог объектов «жрёт память». Начинаем профилировать — и выясняется, что проблема не в тяжёлых изображениях, а в том, что ViewController карточки объекта не деинициализируется после закрытия. Пользователь открыл 10 карточек подряд — и в памяти висит 10 контроллеров, каждый со своей галереей. Это классический сценарий, где утечка маскируется под «тяжёлый контент».

Какие инструменты в Instruments использовать

Для поиска утечек чаще всего нужны два инструмента:

  • Leaks — находит именно утечки, то есть объекты, которые уже не должны быть доступны, но остаются в памяти. Он периодически проверяет память приложения и ищет блоки, на которые нет корректных ссылок.
  • Allocations — показывает, что именно выделяется в памяти, где и когда это происходит. Полезен, когда нужно понять не только факт утечки, но и путь её появления.

На практике лучше использовать их вместе. Leaks отвечает на вопрос «есть ли проблема», а Allocations помогает понять «что именно растёт и откуда пришло».

Добавлю нюанс из опыта: Leaks отлично ловит явные циклы удержания, но может пропустить ситуацию, когда объект формально достижим через цепочку ссылок, хотя по логике приложения уже не нужен. Например, сервис синхронизации каталога недвижимости продолжает висеть в памяти, потому что на него держит ссылку живой singleton, хотя сам сервис уже отработал и должен быть освобождён. В таких случаях Allocations с фильтрацией по классам становится основным инструментом расследования.

Подготовка к проверке

Перед запуском Instruments нужно убрать лишний шум. Иначе можно потратить час на анализ «утечки», которой на самом деле нет.

Что сделать перед профилированием

  • Закрыть лишние приложения на устройстве.
  • Проверять сценарий на одинаковых данных и одинаковом наборе действий.
  • Отключить отладочные логи, если они создают лишнюю нагрузку.
  • Использовать один и тот же маршрут в приложении: например, открыть список объектов, зайти в карточку, вернуться назад.
  • По возможности тестировать на реальном устройстве, а не только в симуляторе.
  • Убедиться, что сборка близка к production-поведению, а не к «тестовой песочнице» с особым логированием и моками.

Отдельно подчеркну важность реального устройства. Симулятор работает с памятью иначе: у него нет ограничений физического iPhone, нет реальной работы с GPU при рендеринге изображений, и сборщик мусора ведёт себя по-другому. Я не раз видел, как утечка, незаметная в симуляторе, становилась очевидной на устройстве после пяти проходов по экрану с галереей объектов.

Базовый сценарий поиска утечки

Самый надёжный подход — повторить подозрительное действие несколько раз и посмотреть, что происходит с памятью.

Пошаговый алгоритм

  1. Запустить приложение из Xcode через режим профилирования.
  2. Выбрать шаблон с Leaks, а при необходимости добавить Allocations.
  3. Нажать запись и выполнить проблемный сценарий.
  4. Повторить одинаковое действие 3–10 раз.
  5. Остановиться и сравнить картину памяти между итерациями.
  6. Проверить, остаются ли объекты после возврата со страницы или закрытия экрана.

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

В контексте PropTech-приложений типичный сценарий выглядит так: открываем каталог объектов недвижимости, заходим в карточку с планировкой и фотографиями, возвращаемся в список, снова заходим в ту же карточку. После 5–7 повторений смотрим на график памяти в Allocations. Если каждый проход добавляет 15–20 МБ, которые не возвращаются — проблема точно есть.

Как читать результаты Leaks

Leaks показывает найденные подозрительные блоки памяти. Обычно здесь важно не только наличие красных маркеров, но и контекст.

На что смотреть в первую очередь

  • Тип объекта и его размер.
  • Стек вызовов, где объект был создан.
  • Фрейм, который указывает на ваш код, а не на системную библиотеку.
  • Повторяемость: появляется ли утечка при каждом запуске сценария.

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

Важный практический момент: когда Leaks показывает утечку в системном фреймворке — например, в UIImage или в AVPlayer — не спешите винить Apple. В 90% случаев системный объект удерживается вашим кодом через замыкание, делегат или таймер, который вы не отпустили. Смотрите на стек вызовов выше системного фрейма — именно там будет ваш метод, который создал объект и не обеспечил его освобождение.

Как использовать Allocations для точной диагностики

Allocations полезен, когда Leak не даёт достаточно ясной картины или когда нужно понять, какие объекты «не умирают» после закрытия экрана.

Что помогает в Allocations

  • Сравнение количества объектов до и после перехода на экран.
  • Просмотр call tree, чтобы понять, какие методы создают больше всего памяти.
  • Фильтрация по типу объектов.
  • Отслеживание жизненного цикла конкретного экрана или модели.

Практически это выглядит так: вы открываете экран, делаете действие, закрываете экран и смотрите, освободились ли связанные объекты. Если модель, view controller или вспомогательный сервис остаются в памяти, проблема обычно не в «тяжёлых данных», а в удержании ссылок.

Приведу пример из реальной практики: в приложении для риэлторов был экран аналитики с графиками цен по районам. После закрытия экрана в памяти оставались объекты PropertyAnalyticsModel — по одному на каждый просмотр. Allocations показал, что модель удерживается через цепочку: ViewController → AnalyticsService → Model, причём AnalyticsService был объявлен как strong-свойство контроллера, а модель — как strong-свойство сервиса. Разрыв цикла через weak-ссылку на сервис решил проблему.

Полезная связка: повторяющийся сценарий и Mark Generation

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

Когда это особенно полезно

  • экран открывается и закрывается многократно;
  • список объектов подгружается по мере скролла;
  • детальная карточка содержит изображения, карты, видео или веб-контент;
  • экран риэлторского приложения подписывается на обновления каталога;
  • в приложении есть офлайн-кэш и синхронизация данных.

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

Техника Mark Generation особенно выручает в приложениях для девелоперов, где экран объекта недвижимости может содержать десятки источников данных: фотографии с сервера, 3D-план этажа, инфографику по инфраструктуре района, динамическую карту с пробками. После отметки поколения перед открытием экрана и после его закрытия вы сразу видите, какие объекты пережили ожидаемый момент освобождения.

Типовые причины утечек в iOS

Ниже — причины, которые чаще всего встречаются в реальных приложениях.

Причина Как проявляется Что проверить
Strong reference cycle Объекты удерживают друг друга Ссылки между controller, view, coordinator, service
Замыкание с сильной ссылкой на self Экран не освобождается после закрытия capture list, async callbacks
Таймер или display link Экран ушёл, а объект остался жить invalidate, stop, deinit
NotificationCenter Объект подписан, но не отписался removeObserver, токены подписки
KVO/наблюдатели Объект не освобождается из-за наблюдения корректное снятие наблюдения
Delegates без weak Цикл удержания между объектами объявление delegate как weak
Кэш и коллекции Память растёт при каждом открытии очистка кэша, лимиты, eviction policy

В контексте недвижимости добавлю специфический кейс: кэш изображений объектов. Если галерея карточки использует NSCache или Dictionary для хранения UIImage, но не настроена политика вытеснения — после просмотра 50 объектов память приложения может вырасти на 300–400 МБ. Это не классический leak с точки зрения Instruments, но с точки зрения пользователя — приложение становится непригодным. Решение: настроить лимит кэша и реагировать на memory warning от системы.

Где искать проблемы в реальном приложении

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

Наиболее рискованные зоны

  • карточка объекта с галереей и вложенными видео;
  • карта с большим числом аннотаций;
  • экран поиска с фильтрами и сохранёнными состояниями;
  • чат с вложениями и превью документов;
  • офлайн-каталог объектов;
  • экран аналитики с графиками и большими JSON-ответами.

Такие экраны часто создают впечатление, что «всё тормозит из-за тяжёлого контента». На деле проблема может быть проще: экран каждый раз создаёт новый набор объектов и не отпускает старый.

Отдельно выделю экран поиска с фильтрами. В риэлторских приложениях это один из самых сложных экранов: пользователь выбирает район, тип недвижимости, диапазон цен, площадь, этажность — и каждый фильтр может быть реализован как отдельный контроллер или ViewModel. Если после применения фильтров и возврата к списку эти объекты не освобождаются, после 10–15 поисковых сессий приложение начнёт тормозить даже на простых операциях.

Как отличить leak от временного роста памяти

Это один из самых частых вопросов у команды.

Признаки утечки

  • память растёт после каждого одинакового действия;
  • после закрытия экрана объекты не освобождаются;
  • leak воспроизводится стабильно;
  • растёт число экземпляров одного и того же класса;
  • In Instruments видно, что объект остаётся жив после ожидаемого момента освобождения.

Признаки нормального поведения

  • память выросла во время действия, а затем снизилась;
  • рост связан с загрузкой изображений, кэшем или анимацией;
  • объект живёт дольше по дизайну, например singleton или общий сервис;
  • после сброса сценария память стабилизируется.

На практике я использую простое правило: если после пяти одинаковых циклов «открыть-закрыть» количество экземпляров ViewController в Allocations равно пяти — это утечка. Если после пятого цикла их всё ещё один или два — вероятно, работает кэширование или пул объектов, и нужно смотреть глубже, но паниковать рано.

Чек-лист для быстрой проверки

  • Повторите один и тот же пользовательский путь несколько раз.
  • Проверьте, растёт ли память ступенчато.
  • Сравните картину до и после закрытия экрана.
  • Посмотрите Leaks на наличие красных срабатываний.
  • Используйте Allocations для поиска конкретного класса.
  • Проверьте замыкания, делегаты, таймеры и подписки.
  • Убедитесь, что экран действительно деинициализируется.
  • Сравните поведение на устройстве и в симуляторе.

Этот чек-лист — результат десятков профилировочных сессий. Он покрывает 80% случаев, с которыми я сталкивался в проектах: от инди-приложений на Objective-C до крупных PropTech-продуктов с многомиллионной аудиторией.

Частые ошибки при анализе

  • Делать вывод по одному запуску, а не по повторяемому сценарию.
  • Искать leak только в Leaks и игнорировать Allocations.
  • Путать кэширование с утечкой.
  • Не проверять deinit у подозреваемого объекта.
  • Анализировать приложение с включённым лишним дебаг-логом и тяжёлыми инструментами.
  • Смотреть только на общий график памяти, а не на конкретные типы объектов.
  • Забывать, что часть объектов может жить осознанно весь цикл приложения.

Пункт про deinit хочу подчеркнуть особо. В Objective-C это был dealloc, и добавить NSLog в него было первым делом при подозрении на утечку. В Swift-проектах я до сих пор использую этот приём: ставлю print в deinit подозрительного ViewController и просто смотрю в консоль при профилировании. Если после закрытия экрана сообщение не появилось — объект жив, и Instruments подскажет почему.

Практический порядок расследования

Рабочая последовательность

  1. Найти экран, после которого растёт память.
  2. Повторить сценарий несколько раз.
  3. Убедиться, что рост воспроизводится.
  4. Открыть Leaks и проверить красные сигналы.
  5. Перейти в Allocations и найти конкретные классы.
  6. Сопоставить аллокации со стеком вызовов.
  7. Проверить ссылки, подписки, таймеры и замыкания.
  8. Исправить причину и прогнать сценарий ещё раз.

Эта последовательность — не теоретическая схема, а рабочий алгоритм, который я применял при отладке приложения для виртуальных шоу-румов. Там экран с 3D-моделью помещения создавал утечку через CADisplayLink, который не инвалидировался при сворачивании экрана. Leaks показал проблему на третьем цикле, Allocations помог найти конкретный таймер, а стек вызовов указал на метод, где display link добавлялся, но не удалялся.

Что делать после исправления

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

  • объект действительно деинициализируется;
  • память перестала расти ступенчато;
  • Leak больше не срабатывает;
  • регрессия не появилась в соседнем экране.

Регрессия — это отдельная боль. Бывает, что исправление цикла удержания в карточке объекта недвижимости ломает освобождение памяти в списке объектов, потому что вы изменили ссылку, которая использовалась в общем сервисе навигации. Поэтому после фикса я всегда прогоняю не только проблемный экран, но и два-три соседних сценария: открыть список, зайти в карточку, вернуться, открыть другую карточку, перейти в фильтры. Это занимает 10 минут, но спасает от повторного баг-репорта через неделю.

Вывод

Instruments остаётся самым надёжным способом найти утечки памяти в iOS-приложении, если использовать его системно, а не «на удачу». Leaks помогает быстро выявить сам факт проблемы, а Allocations показывает, где именно возникает лишнее удержание памяти. Лучший результат даёт не разовая проверка, а повторяемый сценарий, внимательное чтение стека вызовов и проверка реального жизненного цикла экранов.

Для разработчиков PropTech-приложений этот подход особенно важен, потому что экраны с объектами недвижимости — одни из самых тяжёлых в iOS-экосистеме: изображения высокого разрешения, интерактивные карты, 3D-планы, потоковые данные от IoT-датчиков в умных зданиях. Без системной работы с Instruments утечки в таких приложениях неизбежны, а их цена — не просто медленный интерфейс, а потерянные сделки риэлторов и негативный опыт клиентов, просматривающих каталог.

FAQ

Почему утечка не всегда видна сразу?

Потому что объект может накапливаться постепенно: один раз рост незаметен, но после нескольких одинаковых действий проблема становится очевидной. В приложениях для недвижимости это классика: пользователь открыл 3 карточки объектов — память выросла на 40 МБ, но система ещё не отправила memory warning. После 10 карточек рост достигает 150 МБ, и приложение крашится на фоне.

Что лучше использовать сначала: Leaks или Allocations?

Если нужен быстрый ответ — начните с Leaks. Если нужна точная причина — подключайте Allocations и анализ call tree. На практике я запускаю оба инструмента одновременно в одном шаблоне Instruments: Leaks даёт красные флаги в реальном времени, а Allocations собирает данные для глубокого анализа после завершения сценария.

Можно ли доверять проверке только в симуляторе?

Не всегда. Некоторые проблемы проявляются иначе на реальном устройстве, особенно при работе с памятью, изображениями и сетью. Симулятор не эмулирует ограничения физического RAM, не обрабатывает memory warning так же агрессивно и не работает с GPU-ресурсами как реальный iPhone. Для финальной проверки всегда используйте устройство.

Что чаще всего вызывает утечки в iOS?

Сильные циклы ссылок, замыкания с self, таймеры, подписки на уведомления и неправильно настроенные делегаты. В Objective-C-проектах добавлю классику: ручное управление памятью без ARC, где забытый release или лишний retain создавал утечку, которую Instruments ловил мгновенно.

Как понять, что экран не освобождается?

Если после закрытия экрана его объекты остаются в памяти и повторное открытие увеличивает число экземпляров, deinit, скорее всего, не вызывается. Быстрый тест: добавьте print("deinit \(self)") в deinit ViewController, запустите приложение, откройте и закройте экран 5 раз. Если в консоли пять сообщений — всё чисто. Если ни одного — добро пожаловать в Instruments.