Retain и Release в Objective-C: как работает ручное управление памятью

Когда я только начинал в инди-студии, Objective-C был основным языком, а ручное управление памятью — такой же привычкой, как утренний кофе. Сегодня ARC стал стандартом, но старый код никуда не делся: в приложениях для риэлторов, которые живут годами, в библиотеках для работы с каталогами недвижимости, в модулях синхронизации данных с серверов девелоперов. Понимание retain и release — это не просто дань истории, а инструмент, который спасает при миграции, отладке и поддержке проектов, где квадратные метры встречаются с кодом.

Что такое ручное управление памятью в Objective-C

Модель MRR (Manual Retain-Release) строится на простой идее: объект живёт, пока у него есть хотя бы один владелец. Каждый вызов retain увеличивает счётчик ссылок, каждый release — уменьшает. Когда счётчик падает до нуля, объект деаллоцируется, освобождая память.

Если перевести на язык ответственности:

  • retain — «я беру этот объект под свой контроль, не удаляйте его»;
  • release — «я больше за него не отвечаю»;
  • autorelease — «освободи его чуть позже, когда будет удобно».

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

Как работает счетчик ссылок

У каждого объекта есть внутренний счётчик владения. При создании через alloc, new, copy или mutableCopy вызывающий код сразу становится владельцем. Дальше — классическая цепочка: одни части приложения удерживают объект (retain), другие отпускают (release), и когда владельцев не остаётся, вызывается dealloc.

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

Правило владения: где брать и где отпускать

Главная ментальная модель в MRR — не «сколько сейчас ссылок», а «кто отвечает за жизнь объекта». Это особенно важно в мобильных приложениях для риэлторов, где один и тот же объект недвижимости может путешествовать через фильтры, избранное, карточку и кеш.

Когда нужно удерживать объект

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

  • сохраняете объект в ivar или свойство — например, выбранную квартиру для детального просмотра;
  • передаёте объект между слоями приложения — из сетевого слоя в UI-модель;
  • объект нужен после завершения текущего метода — скажем, результат парсинга JSON с описанием ЖК;
  • объект пришёл как временный, но потребуется асинхронно — например, в колбэке загрузки изображения.

Когда нужно освобождать объект

release вызывают, когда объект больше не нужен и вы отказываетесь от владения. В контексте недвижимости это происходит постоянно:

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

Минимальный жизненный цикл объекта

Классическая схема работы с объектом в MRR выглядит так: создание через alloc/init → удержание при необходимости → использование → освобождение, когда владение больше не требуется → автоматический вызов dealloc при нулевом счётчике.

В реальных проектах ручное прописывание всех шагов встречается реже, чем кажется. Чаще retain и release прячутся в сеттерах, фабричных методах и dealloc. Например, фабричный метод, возвращающий объект квартиры с autorelease, позволяет вызывающему коду не думать о немедленном освобождении, но требует понимания времени жизни пула автосбора.

Где чаще всего ошибаются

За годы работы с legacy-кодом в приложениях для девелоперов я выделил четыре основные ловушки.

  1. Утечка памяти. Объект получил владение, но release так и не вызвали. В каталоге с тысячами объектов это быстро приводит к тому, что приложение начинает «тормозить» и вылетать по памяти.
  2. Перелиз. Объект освободили раньше времени или лишний раз. Классика: колбэк обращается к уже удалённой модели данных, и приложение падает с EXC_BAD_ACCESS. Особенно коварно проявляется при быстрой смене фильтров в поиске недвижимости.
  3. Висячая ссылка. Переменная продолжает указывать на уничтоженный объект. В старом коде с коллекциями, делегатами и callback-логикой это настоящий рассадник крашей. Например, делегат экрана карточки объекта был освобождён, а кнопка «Позвонить» ещё пытается ему что-то сообщить.
  4. Ошибки в сеттерах. Сначала присвоили новое значение, а старое не освободили. Так при каждом обновлении свойства объекта недвижимости (скажем, цены) память утекает незаметно, но неумолимо.

Как писать сеттеры в MRR

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

На практике это выглядит так: сначала выполняется [newValue retain], затем [oldValue release], и только потом _property = newValue. Такой порядок гарантирует, что если вдруг в процессе освобождения старого объекта что-то пойдёт не так, новое значение уже надёжно удержано. В проекте для агентства недвижимости, где свойства объектов активно обновлялись из разных потоков при синхронизации, этот паттерн спасал от трудновоспроизводимых гонок данных.

Что делает autorelease

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

Типичный сценарий из моей практики: метод парсит XML с описанием объектов недвижимости и создаёт десятки временных строк для адресов, цен, описаний. Без @autoreleasepool внутри цикла память росла стремительно, и на старых устройствах приложение падало ещё до окончания импорта. Оборачивание тела цикла в пул автосбора решало проблему кардинально — пиковые значения памяти снижались в разы.

Чем retain отличается от copy

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

Метод Что делает Когда использовать
retain Увеличивает счётчик ссылок на существующий объект Когда нужен тот же самый объект, и его изменение извне допустимо
copy Создаёт независимую копию и передаёт владение ей Когда исходный объект может измениться, а вам нужна стабильная версия (например, строка адреса)
release Уменьшает счётчик ссылок Когда объект больше не нужен
autorelease Откладывает освобождение до ближайшего пула Для удобной передачи временных объектов без ручного управления

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

Как выглядит dealloc

В MRR метод dealloc — это место, где вы прощаетесь со всеми объектами, которыми владел экземпляр класса. Здесь критичен порядок: сначала освобождаются собственные объекты, потом вызывается [super dealloc].

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

Когда retain и release реально нужны сегодня

ARC стал индустриальным стандартом, но старый код на Objective-C никуда не делся. Особенно это касается долгоживущих iOS-приложений для риэлторов и девелоперов, где обновление часто откладывается из-за плотного графика релизов. С какими случаями я сталкиваюсь регулярно:

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

Для разработчика это означает одно: даже если в новом коде вы не пишете retain/release, понимать их нужно, чтобы поддерживать чужие проекты, проводить миграцию без регрессий и не бояться заглядывать в тёмные углы рантайма.

Как безопасно мигрировать код на ARC

Apple отмечает, что ARC автоматически вставляет нужные вызовы управления памятью на этапе компиляции, а Xcode предлагает инструмент миграции. Но автоматика не всесильна. Практический порядок, выработанный на нескольких проектах для рынка недвижимости, выглядит так:

  1. Проверить, нет ли ручного освобождения в ключевых местах — особенно в dealloc и сеттерах.
  2. Убрать явные вызовы retain, release, autorelease там, где это допустимо.
  3. Перепроверить свойства: strong, weak, copy должны соответствовать логике владения. Часто старые assign для объектов становятся бомбой замедленного действия.
  4. Пройтись по dealloc, делегатам и блокам — здесь любят прятаться циклы владения, которые ARC сам не разрешит.
  5. Протестировать сценарии, где объекты живут дольше одного экрана или одного запроса: кеш изображений, долгие синхронизации, фоновые загрузки.

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

Чек-лист для проверки старого кода

  • Везде ли соблюдено правило: кто создал или удержал, тот и отвечает за освобождение.
  • Нет ли пар retain без соответствующего release — особенно в ветвлениях и досрочных выходах из методов.
  • Нет ли лишних release в dealloc или сеттерах, которые могут привести к перелизу.
  • Не сохраняются ли временные объекты без удержания — например, результат autorelease присваивается ivar без retain.
  • Не используется ли autorelease в горячих циклах без пула — верный путь к пикам памяти.
  • Корректно ли реализованы свойства с retain и copy — не подменяются ли они случайно.
  • Нет ли опасных обращений к объектам после освобождения — проверьте все делегаты и callback’и.
  • Не осталось ли свойств с модификатором assign для объектов, которые должны быть strong или weak.

Типовые практические сценарии

Каталог объектов в мобильном приложении

Когда приложение риэлтора хранит карточки квартир, фильтры и временные результаты поиска, управление памятью напрямую влияет на плавность скроллинга. В MRR важно не держать лишние объекты в памяти: если каталог содержит тысячи элементов с изображениями, адресами, ценами и метаданными, то каждая забытая release для ячейки, ушедшей за экран, быстро приводит к тому, что приложение начинает «задыхаться». Я всегда следил, чтобы при переиспользовании ячеек старые данные освобождались, а новые удерживались только на время отображения.

Синхронизация данных

При загрузке данных с сервера — например, полного дампа объектов недвижимости в формате JSON — создаются десятки тысяч временных моделей. Если не освобождать промежуточные словари и массивы, приложение раздувается в памяти до гигабайта за считанные секунды. В одном проекте для девелопера мы парсили огромный XML с планировками: без локальных @autoreleasepool внутри циклов память улетала в потолок, и старые iPhone просто убивали приложение. Правильное управление пулами и своевременные release позволяли держать потребление в рамках 50–70 МБ даже на полной выборке.

Офлайн-просмотр

Когда пользователь открывает карточку объекта без сети, экран может держать сразу несколько вложенных моделей: галерею фото высокого разрешения, интерактивную карту, подробное описание, характеристики. Здесь особенно опасны циклы владения и забытые release. Я не раз сталкивался с тем, что после пролистывания десятка карточек приложение вылетало по памяти — потому что полноразмерные изображения не освобождались при уходе с экрана. В MRR приходилось вручную чистить кеш в dealloc и следить, чтобы каждый retain на фото имел парный release.

Сбор данных с IoT-устройств в умном здании

В системах мониторинга недвижимости, где датчики температуры, влажности и энергопотребления шлют данные каждую секунду, объекты-измерители создаются и удаляются постоянно. Если фоновый сервис написан на Objective-C с ручным управлением памятью, любая утечка в цепочке обработки пакета приводит к неуклонному росту потребления памяти и падению сервиса через несколько часов. При миграции такого модуля на ARC важно было сохранить все ручные release до полной проверки, потому что датчики генерировали миллионы временных объектов в сутки, и даже небольшая ошибка быстро становилась критичной.

Главное, что нужно запомнить

  • retain увеличивает право на владение объектом.
  • release отказывается от этого права.
  • Когда счётчик ссылок падает до нуля, объект уничтожается.
  • autorelease откладывает освобождение до ближайшего пула.
  • В MRR важнее понимать ownership, чем смотреть на retainCount.
  • Сегодня ARC делает большую часть работы автоматически, но старый код всё ещё требует понимания ручной модели.
  • Даже в ARC-проектах знание retain/release помогает проектировать графы владения без циклов и утечек, особенно при работе с блоками и делегатами.

FAQ

Что такое retain простыми словами?

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

Что будет, если забыть release?

Объект останется в памяти дольше, чем нужно, и это приведёт к утечке памяти. В приложении для риэлтора такая утечка на каждом переходе в карточку объекта быстро приведёт к тому, что через 20–30 просмотров приложение упадёт.

Можно ли вызывать release дважды?

Нет. Повторный release на уже освобождённом объекте обычно приводит к крашу. Рантайм не прощает таких ошибок, и отладка может занять часы.

Зачем тогда нужен ARC?

ARC избавляет от ручного подсчёта ссылок: компилятор сам вставляет нужные вызовы управления памятью. Это радикально снижает количество ошибок и ускоряет разработку, но не отменяет необходимости понимать, что происходит под капотом.

Нужно ли знать retain/release, если пишешь только под ARC?

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

Где чаще всего ломается память в старом Objective-C?

В сеттерах, dealloc, асинхронных колбэках, делегатах и при работе с временными объектами в циклах. Именно эти места я проверяю в первую очередь, когда беру в руки проект с историей.