ARC в Objective-C — это не волшебная таблетка от утечек памяти. Да, компилятор сам расставляет retain и release, но как только в проекте появляются сложные графы объектов, делегаты и замыкания, начинаются проблемы. Большинство неприятностей связаны не с самим механизмом подсчёта ссылок, а с ошибками в проектировании связей между объектами, блоками и наблюдателями. А когда речь заходит о приложениях для рынка недвижимости — с десятками экранов, каталогами, push-уведомлениями и фоновой синхронизацией — даже мелкая архитектурная оплошность быстро превращается в ощутимый рост памяти и нестабильность.
Что ARC делает, а что — нет
ARC автоматически вставляет retain и release на этапе компиляции, избавляя разработчика от рутины ручного управления памятью. Но он не анализирует граф владения и не умеет разрывать циклы. Это как автоматизированная система учёта договоров аренды: она следит за сроками, но не проверит, не заключили ли вы два взаимоисключающих контракта на один объект. Если два объекта держат друг друга сильными ссылками, ARC не вмешается — он просто будет считать, что оба живы и нужны.
Простое правило
- ARC отвечает за автоматическое управление счётчиками ссылок.
- Разработчик отвечает за архитектуру владения.
- Если есть цикл, его нужно разорвать вручную через
weak,unsafe_unretainedили пересмотр логики владения.
Типичные ошибки при ARC
1. Сильная ссылка там, где нужна слабая
Одна из самых частых ошибок — объявить delegate как strong вместо weak. В приложениях для риэлторов это всплывает постоянно: контроллер карточки объекта держит view model, та — своего делегата, а делегатом часто выступает сам контроллер. Если связь сильная с обеих сторон, образуется классический цикл, и экран никогда не освобождается. Пользователь за день открывает сотни карточек, память неумолимо растёт, а девелопер разводит руками.
Как правильно:
- для
delegateпочти всегда использоватьweak; - для ссылок на владельца или координатор — проверять направление владения: кто кем должен распоряжаться;
- не полагаться на ARC, если объект должен быть наблюдателем, а не владельцем.
2. Захват self в блоке без weak/strong
Блоки — один из главных источников retain cycle в Objective-C. Если self удерживает блок (например, сохраняет его в свойстве), а блок захватывает self сильно, объект не деаллоцируется. В CRM для агентств недвижимости типичный сценарий: completion-блок сохранения избранного объекта или обновления списка хранится как свойство контроллера, внутри блока идёт обращение к self для обновления UI. Контроллер уходит с экрана, но блок продолжает его держать — утечка.
Решение: использовать паттерн __weak typeof(self) weakSelf = self; и затем __strong typeof(weakSelf) strongSelf = weakSelf; внутри блока. Это гарантирует, что блок не продлит жизнь self сверх необходимости, но если объект ещё жив на момент выполнения, он будет доступен.
3. Подписка на уведомления без отписки
Объект может оставаться в памяти дольше, чем ожидается, если не снять его с NSNotificationCenter, KVO или других механизмов наблюдения. ARC тут бессилен: наблюдатель продолжает быть связанным с источником событий, и система считает его нужным. В девелоперских приложениях это часто проявляется с push-уведомлениями о новых объектах или изменении статуса сделки: экран закрыт, а подписка жива и тянет за собой весь контроллер.
4. Неверные ожидания от dealloc
Если dealloc не вызывается, это не всегда «утечка в ARC», но почти всегда сигнал о цикле или неразорванной связи. Проверка dealloc — один из самых быстрых способов понять, живёт ли объект дольше нормы. В проектах, где экран списка объектов открывается и закрывается многократно, отсутствие вызова dealloc после ухода с экрана — верный признак того, что где-то осталась сильная ссылка.
5. Over-release в старом коде после миграции
При миграции legacy-кода на ARC проблемы часто возникают не в новом коде, а на границе с ручным управлением памятью. Это особенно заметно в проектах с долгой историей — например, в модулях расчёта ипотеки или старых утилитах для работы с API, которые писались ещё до повсеместного внедрения ARC. Категории, синглтоны, самописные контейнеры — всё это может нести скрытые ручные retain/release, которые конфликтуют с автоматикой.
6. Ошибки с block copy и свойствами
Блоки нужно хранить корректно. Если блок живёт дольше стека, а property или контейнер настроены неправильно, можно получить неожиданные падения или утечки. В ARC это проявляется реже, чем во времена ручного управления, но старые шаблоны кода всё ещё встречаются — например, когда блок передаётся в асинхронный вызов и сохраняется в коллекции без копирования. В сетевых слоях приложений для загрузки каталогов недвижимости такие ошибки приводят к трудновоспроизводимым крешам.
Самые частые симптомы проблемы
| Симптом | Что это может значить | Что проверять |
|---|---|---|
Контроллер не вызывается в dealloc |
retain cycle | strong-ссылки, блоки, delegate, observers |
| Память растёт после переходов по экранам | накопление неосвобождённых объектов | живые экземпляры в heap, удержание через closures |
| Краш при обращении к уже освобождённому объекту | over-release или обращение к dangling pointer | Zombies, место вызова, асинхронные callbacks |
| Объект живёт после закрытия экрана | кто-то удерживает его сильной ссылкой | Memory Graph, Instruments, свойства и блоки |
В приложениях для риэлторов эти симптомы обостряются при массовой навигации: когда пользователь за сессию просматривает десятки объектов, даже небольшое удержание каждого экрана быстро превращается в десятки мегабайт лишней памяти и заметные тормоза.
Как диагностировать проблемы в ARC
1. Начните с dealloc
Самый простой и полезный шаг — добавить лог в dealloc и проверить, вызывается ли он после закрытия экрана. Если объект не освобождается, значит, где-то есть сильная ссылка, которую не видно на первый взгляд.
Практика:
- откройте экран (например, карточку объекта недвижимости);
- выполните действие — сохраните в избранное, откройте фотогалерею;
- закройте экран;
- проверьте, появился ли лог
dealloc; - если нет — ищите цикл.
2. Используйте Memory Graph Debugger
Граф памяти помогает увидеть, какие объекты удерживают друг друга. Это особенно полезно при поиске циклов между контроллером, view model, callback-блоком и вспомогательными объектами. В проектах для девелоперов типичная картина: контроллер карточки объекта → view model → блок обновления → контроллер. Memory Graph покажет эту цепочку наглядно.
Что искать:
- объект, который должен был исчезнуть;
- кто держит его strong-ссылкой;
- есть ли цепочка
A -> B -> A.
3. Проверяйте Instruments
Instruments полезен, когда нужно понять динамику памяти, а не только статическую картину. Для ARC особенно полезны Allocations и Leaks. Сравнение снимков до и после перехода по экрану даёт объективную картину: если после 20 открытий карточки объекта память выросла на 30–50 МБ и не вернулась к исходному уровню, утечка налицо.
В одном из типичных сценариев диагностика через Allocations с отслеживанием живых объектов показывает, что после серии переходов экземпляры контроллера продолжают копиться и не деаллоцируются. В риэлторских CRM это часто связано с блоками обновления списка, которые держат контроллер, или с подписками на изменения избранного.
4. Включайте Zombies для падений на освобождённых объектах
Если приложение падает с обращением к уже освобождённому экземпляру, включение Zombies помогает понять, какой объект был затронут. Это особенно полезно при ошибках вида «message sent to deallocated instance».
Когда использовать:
- падения после асинхронных операций (например, загрузка фотографий объекта);
- обращение к объекту после закрытия экрана;
- неустойчивые креши, которые сложно поймать обычным дебагом.
5. Смотрите на retain history
Если объект удерживается слишком долго, полезно проследить его историю владения. В инструментах профилирования можно увидеть, где объект получил лишнюю ссылку и почему не освобождается. Это особенно актуально для объектов, связанных с таймерами или циклическими опросами IoT-датчиков в умных зданиях: датчик шлёт данные, подписка держит контроллер, а инвалидация не предусмотрена.
Пошаговый алгоритм поиска утечки
- Повторите сценарий несколько раз: откройте и закройте экран 5–10 раз. В реальном проекте для агентства недвижимости мы специально прогоняли цикл открытия карточки объекта, добавления в избранное и возврата в список.
- Добавьте лог в
dealloc. - Откройте Memory Graph и найдите «зависший» объект.
- Проверьте
delegate,block,timer, observers, completion handlers. - Если есть асинхронщина, проверьте, не удерживает ли очередь или операция
self. - Сравните поведение с Instruments по памяти.
- Если есть crash — включите Zombies и посмотрите источник обращения.
На что смотреть в первую очередь
Чек-лист диагностики
delegateобъявлен какweak, если объект не должен владеть делегатом.- Блоки не захватывают
selfбез необходимости. - Таймеры и диспетчерские источники корректно инвалидируются.
- Подписки на уведомления снимаются.
- KVO удаляется до уничтожения объекта.
deallocдействительно вызывается.- После перехода по экрану память возвращается к базовому уровню.
Таблица: ошибка, признак и способ исправления
| Ошибка | Как проявляется | Исправление |
|---|---|---|
delegate как strong |
объект не освобождается | заменить на weak |
Захват self в block |
retain cycle | использовать weak/strong dance |
| Не сняты observers | неожиданные вызовы, утечки | убрать подписки в нужный момент |
| Таймер не остановлен | объект живёт дольше экрана | invalidate/stop |
| Неправильная работа с async callback | креши или утечки | ослабить захват self, проверить lifecycle |
| Legacy-код после миграции | нестабильная память | проверить границы ARC/MRR и внешние библиотеки |
Типовые ошибки в проектах для недвижимости и бизнес-приложений
В приложениях для риэлторов, каталогов объектов и CRM утечки часто появляются не в «учебных» местах, а в реальных сценариях, которые сложно воспроизвести на синтетических тестах:
- экран карточки объекта открывается десятки раз за день — каждый раз создаётся новый контроллер, и если старый не освобождается, память утекает стремительно;
- блоки обновления списка держат контроллер, особенно когда список перезагружается по таймеру или при получении push-уведомления о новых объектах;
- подписки на push-обновления остаются активными после ухода со страницы — контроллер продолжает получать события и не может быть освобождён;
- синхронизация каталога создаёт длинные цепочки удержания: менеджер синхронизации → операция → блок → контроллер;
- в виртуальных шоу-румах и 3D-турах объект загружает модель, подписывается на обновления геометрии и забывает отписаться при закрытии — результат тот же.
В таких проектах особенно важно тестировать сценарии с повторным открытием экранов и массовой навигацией, потому что мелкая утечка на одном экране быстро превращается в заметный рост памяти. Инструменты диагностики должны быть частью регулярного регрессионного тестирования, а не разовой акцией.
Как писать код, который легче диагностировать
- Давайте объектам понятные роли: владелец, наблюдатель, посредник — как в управлении недвижимостью есть собственник, арендатор и агент. Это помогает сразу определить, кто кого должен удерживать.
- Не храните блоки дольше, чем нужно. Если блок используется однократно, не сохраняйте его в свойство.
- Разрывайте связи на границе жизненного цикла экрана: в
viewDidDisappearилиdeallocявно обнуляйте делегатов, отписывайтесь от уведомлений, останавливайте таймеры. - Проверяйте
deallocна ключевых контроллерах — это как регулярный осмотр фундамента здания. - Тестируйте память на реальных пользовательских сценариях, а не только на одном запуске. Прогоняйте типовые маршруты: поиск объекта, просмотр карточки, добавление в избранное, возврат, снова поиск.
FAQ
Почему ARC не убирает все утечки?
Потому что ARC управляет счётчиками ссылок, но не анализирует граф владения и не разрывает циклы. Это как автоматический подсчёт арендаторов в здании: система знает, сколько людей зашло и вышло, но не видит, что двое держат друг друга за руки и не могут разойтись.
Как быстро проверить, есть ли retain cycle?
Самый быстрый способ — посмотреть, вызывается ли dealloc, а затем открыть Memory Graph и найти удерживающую цепочку. В риэлторском приложении это занимает минуту: открыли карточку, закрыли, нет лога — смотрим граф, видим цикл через блок.
Что чаще всего вызывает утечки в Objective-C под ARC?
delegate со strong-ссылкой, блоки с захватом self, observers, KVO и таймеры. Именно эти элементы формируют большинство циклов в бизнес-приложениях.
Когда включать Zombies?
Когда есть подозрение на обращение к уже освобождённому объекту и приложение падает нестабильно. Например, после асинхронной загрузки изображения для карточки объекта контроллер уже ушёл, а callback пытается обновить UI.
Что использовать в первую очередь: Memory Graph или Instruments?
Если нужен быстрый ответ по одной утечке — Memory Graph. Он покажет статическую картину и цепочку владения. Если нужна системная картина и динамика роста памяти — Instruments, особенно Allocations с генерациями.
Вывод
ARC в Objective-C снимает рутинную работу с retain и release, но не избавляет от ошибок проектирования памяти. Основные проблемы почти всегда сводятся к retain cycle, неправильным delegate, захвату self в блоках и забытым подпискам. В приложениях для рынка недвижимости, где экраны открываются часто, а данные обновляются в реальном времени, эти ошибки стоят дорого: от падения производительности до негативных отзывов пользователей.
Лучший подход — проверять dealloc, использовать Memory Graph и Instruments, а при крешах подключать Zombies. Именно такая связка даёт быстрый и надёжный способ находить проблемы в реальных iOS-проектах. И помните: ARC — это фундамент, но за надёжность конструкции отвечает архитектор.
