Переход с manual retain/release на ARC — это не просто «убрать лишние вызовы из кода». Это изменение правил владения объектами, которое влияет на dealloc, свойства, блоки, Core Foundation и даже на то, как вы читаете старый код. Когда я впервые переносил на ARC приложение для риэлторов с каталогом на 30 000 объектов, мы обнаружили, что грамотная миграция не только сокращает количество крашей из-за обращений к освобождённой памяти, но и делает проект прозрачнее для новых разработчиков. При этом важно понимать: ARC не волшебная кнопка, а смена модели мышления, где компилятор берёт на себя рутину, а вы — ответственность за архитектуру владения.
Что меняется при переходе на ARC
ARC автоматически вставляет нужные вызовы управления памятью на этапе компиляции, вместо того чтобы вы делали это вручную. Это означает, что в ARC-коде нельзя напрямую вызывать retain, release и autorelease. На практике переход меняет не только синтаксис, но и мышление: раньше вы контролировали каждый объект вручную, теперь вы описываете владение через свойства, указатели и семантику мостов, а компилятор берёт на себя большую часть рутины.
В проектах, связанных с недвижимостью, где мобильное приложение часто работает с десятками мегабайт фотографий объектов, офлайн-картами и потоками данных от IoT-датчиков, ручное управление памятью быстро становится узким местом. Ошибка в одном release может привести к падению приложения у риэлтора на выезде, когда он показывает клиенту планировку. ARC снимает этот класс проблем, но требует аккуратной настройки bridging и работы с блоками.
Когда ARC особенно полезен
- в больших legacy-проектах, где ручное управление памятью уже стало источником регрессий;
- при развитии iOS-приложений с большим количеством моделей, экранов и асинхронных сценариев — например, синхронизация каталога недвижимости с CRM, загрузка панорам 360° или виртуальных туров;
- если проект долго живёт и в нём работают разные команды — типичная ситуация для стартапов в PropTech, которые вырастают из инди-разработки в продукт с несколькими программистами;
- когда нужно снизить риск утечек и обращений к уже освобождённым объектам, особенно на экранах с интенсивным обновлением данных (списки объектов, карточки с медиа).
С чего начать миграцию
Apple рекомендует использовать встроенный инструмент миграции Xcode, который автоматически убирает механические части конвертации и подсказывает, что требует ручной правки. Для запуска используется пункт меню Edit → Refactor → Convert to Objective-C ARC. Но прежде чем нажать эту кнопку, стоит подготовиться — особенно если проект пережил несколько поколений разработчиков и в нём есть код, написанный ещё до появления ARC.
Я обычно начинаю с аудита: какие части приложения наиболее чувствительны к памяти, где используются Core Foundation-объекты (например, рендеринг PDF-планов этажей через CGPDFDocument), где есть ручные менеджеры памяти. В проектах для девелоперов часто встречаются самописные кэши изображений, которые держат CFMutableDictionaryRef — их миграция требует особого внимания.
Чек-лист перед миграцией
- Зафиксировать состояние проекта в системе контроля версий.
- Прогнать сборку и тесты до изменений.
- Проверить, нет ли файлов, которые должны остаться без ARC (например, старые си-библиотеки для работы с геоданными или проприетарные модули от партнёров).
- Найти места с Core Foundation и кастомными bridging-операциями — особенно если приложение интегрируется с картами, обрабатывает метаданные объектов или использует низкоуровневые API для работы с файловой системой.
- Отдельно отметить категории, ручные менеджеры памяти и код с C API.
- Подготовить список модулей, которые завязаны на
deallocи деликатное освобождение ресурсов (например, закрытие потоков для загрузки фотографий или остановка мониторинга геозон).
Если проект большой, лучше мигрировать поэтапно: сначала стабильный слой модели и UI, затем сервисы, потом низкоуровневые интеграции. В одном из наших проектов для агентства недвижимости мы начали с экрана списка объектов и карточки — это дало быстрый эффект по стабильности, а модуль синхронизации с сервером перенесли позже, когда убедились, что основные сценарии работают без регрессий.
Что делает мигратор автоматически
Инструмент конвертации обычно:
- удаляет
retain,release,autorelease; - убирает
deallocтам, где он больше не нужен; - удаляет
[super dealloc]; - заменяет устаревшие атрибуты свойств на ARC-эквиваленты;
- подсказывает, где нужен ручной контроль семантики владения.
Это удобно, но важно понимать: мигратор не «понимает» архитектуру вашего приложения. Он исправляет механику, а не бизнес-логику. Например, он не знает, что делегат должен быть weak, чтобы избежать цикла удержания с родительским контроллером, который управляет карточкой объекта недвижимости. Поэтому часть мест почти всегда придётся проверить руками.
Что нужно исправить вручную
1. Свойства retain и assign
В ARC вместо retain используют strong, а вместо небезопасного assign для объектов — weak. Это один из первых и самых заметных переходов. В приложениях для риэлторов, где контроллеры активно обмениваются данными через делегаты и замыкания, неправильный выбор между strong и weak часто приводит к циклическим ссылкам и утечкам памяти при навигации между экранами.
| Старый стиль | ARC-стиль | Комментарий |
|---|---|---|
retain |
strong |
Объект удерживается свойством |
assign |
weak |
Для избежания dangling pointer |
copy |
copy |
Остаётся актуальным для строк, блоков и коллекций |
Особенно внимательно проверяйте:
- делегаты — почти всегда должны быть
weak; - outlets — Xcode по умолчанию делает их
weak, но в старом коде могут бытьassign; - ссылки на родительские объекты — если дочерний контроллер держит сильную ссылку на родителя, при закрытии экрана память не освободится;
- блоки, которые захватывают
self— здесь нужна осознанная работа сweak/strongвнутри блока, особенно в асинхронных загрузках каталогов.
2. dealloc
В ARC dealloc может оставаться, но без ручных вызовов release и [super dealloc]. Если в dealloc у вас были cleanup-операции, например закрытие файлов, остановка наблюдателей или освобождение C-ресурсов, их нужно сохранить, но адаптировать под ARC. В проектах с виртуальными шоу-румами мы часто использовали dealloc для освобождения временных хранилищ скачанных 3D-моделей — в ARC это остаётся необходимым, просто без ручного управления Objective-C-объектами.
Типичные действия внутри dealloc:
- снять
NSNotificationCenterobserver; - остановить KVO;
- закрыть файловые дескрипторы (например, для кэша изображений);
- освободить
malloc-память; - обнулить низкоуровневые ресурсы.
3. Core Foundation и bridging
ARC не управляет Core Foundation автоматически так же, как Objective-C-объектами. Если в проекте есть CFTypeRef, CGColorRef, CFDictionaryRef, старые C API или ручной обмен между мирами Core Foundation и Cocoa, придётся использовать bridging. Это особенно актуально для приложений, которые рендерят планы помещений через Core Graphics или работают с геоданными через низкоуровневые библиотеки.
Основные bridging-операции:
__bridge— без передачи владения;__bridge_transfer— передать владение ARC;__bridge_retained— передать владение в сторону Core Foundation.
Это одна из самых частых причин скрытых утечек после миграции. Например, если вы создаёте CFStringRef из NSString для передачи в C-функцию и не используете правильный bridging, ARC может либо преждевременно освободить объект, либо никогда его не освободить.
Главные риски при переходе
Утечки из-за неверного bridging
Если объект передан в CF-мир без корректной передачи владения, ARC его не освободит как надо. Если наоборот — вы передали владение и забыли про балансировку, получите утечку или двойное освобождение. В одном проекте для умного здания мы столкнулись с утечкой при передаче данных с IoT-датчиков через C-обёртку: каждое обновление показаний создавало новый CFDataRef, который не освобождался, и за сутки приложение потребляло гигабайт памяти.
Циклы удержания
ARC не решает проблему retain cycle автоматически. Если два объекта ссылаются друг на друга сильными ссылками, память не освободится. В риэлторских приложениях это часто проявляется на экранах с фильтрами: контроллер держит модель фильтра, модель через замыкание — контроллер, и при каждом открытии экрана расход памяти растёт.
Частые сценарии:
- контроллер удерживает view model, а view model удерживает контроллер;
- блок захватывает
self, аselfхранит блок; - делегат по ошибке сделан
strong, а неweak.
Категории и файлы без ARC
В крупных проектах часть файлов может жить в режиме без ARC. Это допустимо: проект можно собирать с разными настройками для отдельных файлов. Такой подход полезен, если у вас есть старый модуль, который пока опасно трогать полностью — например, самописный рендерер карт, интегрированный с проприетарным SDK девелопера. Мы обычно помечаем такие файлы флагом -fno-objc-arc в настройках компиляции и постепенно заменяем их на ARC-совместимые аналоги.
Как мигрировать по шагам
Шаг 1. Сделайте карту памяти проекта
Разбейте код на группы:
- модели (объекты недвижимости, фильтры, метаданные);
- UI-слой (контроллеры списков, карточек, галерей);
- сетевой слой (загрузка каталогов, синхронизация, API);
- интеграции с Core Foundation (рендеринг планов, работа с геоданными);
- утилиты и категории;
- старые модули без тестов.
Сначала переводите наиболее безопасные части — модели и UI, где бизнес-логика не так сильно завязана на ручное управление памятью. В проекте для агентства недвижимости мы начали с моделей данных (Property, Agent, Deal), затем перешли к контроллерам, и только потом — к сетевым менеджерам.
Шаг 2. Запустите автоматическую конвертацию
Используйте встроенный migrator Xcode. После этого не спешите коммитить результат без проверки — автоматика часто пропускает неочевидные места, например, блоки с неявным захватом self в циклах.
Шаг 3. Проверьте warnings и ошибки сборки
После миграции компилятор часто подсвечивает проблемные участки:
- вызовы запрещённых методов управления памятью;
- неверные свойства;
- неправильные bridging-касты;
- блоки с неочевидным захватом объектов.
Не игнорируйте предупреждения — за каждым из них может стоять потенциальная утечка или падение в production.
Шаг 4. Пройдитесь по критическим сценариям руками
Особенно важно протестировать:
- открытие и закрытие экранов — после выхода из карточки объекта память должна освобождаться;
- повторное создание контроллеров — например, быстрое переключение между списком и картой;
- асинхронные загрузки — что происходит, если пользователь ушёл с экрана до завершения запроса;
- подписки на уведомления — не остаются ли наблюдатели после деаллокации контроллера;
- массовое обновление списков и кэшей — типичный сценарий при подгрузке новых объектов в каталоге.
Шаг 5. Прогоните инструменты диагностики
После миграции полезно проверить:
- утечки (Leaks в Instruments);
- dangling pointer-сценарии (Zombies);
- retain cycle в блоках (статический анализатор Xcode);
- деаллокацию ключевых экранов (логирование в
dealloc); - поведение под нагрузкой (симуляция долгой работы приложения с периодическим обновлением данных).
Типовые ошибки при миграции
- Оставили
retain/releaseв части файлов после частичной конвертации — компилятор выдаст ошибку, но если файл исключён из ARC, ошибки не будет, а логика сломается. - Заменили
assignнаstrongтам, где нуженweak— классический пример с делегатом, который начинает удерживать родительский контроллер. - Убрали
dealloc, хотя там была важная очистка ресурсов — например, закрытие потоков для загрузки изображений объектов. - Не проверили CF-объекты и получили скрытые утечки — особенно коварно в модулях работы с PDF-планами или картами.
- Не учли блоки, которые удерживают
self— в асинхронных коллбэках после миграции это становится главной причиной роста памяти. - Перенесли код в ARC, но оставили старые assumptions о времени жизни объектов — например, ручное обнуление переменных перед выходом из метода, которое теперь избыточно или даже вредно.
Практический пример: что обычно ломается первым
В старом коде часто встречается паттерн: контроллер создаёт массив объектов (например, список предложений недвижимости), вручную удерживает его через retain, потом освобождает в dealloc. После миграции эта логика должна жить уже на уровне strong-свойств или локальных сильных ссылок. Если при этом в коде есть асинхронный callback, который держит self, легко получить цикл удержания. В такой ситуации weak-ссылка на self внутри блока — не формальность, а обязательная мера.
В одном из приложений для риэлторов мы обнаружили, что после миграции экран списка объектов перестал освобождаться при уходе назад. Оказалось, что блок загрузки данных захватывал self сильно, а контроллер хранил этот блок в свойстве. Решение — классическое: создать __weak ссылку перед блоком и использовать её внутри, а при необходимости усилить до strong на время выполнения.
Что особенно важно для старых iOS-проектов в недвижимости и PropTech
В мобильных продуктах для риэлторов, каталогов объектов и выездных приложений часто есть:
- тяжёлые списки с фото и PDF — при прокрутке каталога из 10 000 объектов память может расти линейно, если не освобождать невидимые ячейки;
- офлайн-кэш — изображения и метаданные хранятся локально, и при некорректном управлении памятью кэш может разрастись до гигабайтов;
- загрузка больших карточек объектов — планировки, панорамы, 3D-туры, которые требуют аккуратного освобождения после закрытия экрана;
- интеграции с картами и геосервисами — постоянное обновление маркеров на карте, геозоны, трекинг перемещений;
- синхронизация с CRM и сервером — фоновые процессы, которые могут работать часами и должны корректно обрабатывать смену состояний.
Такие проекты особенно чувствительны к утечкам. Если приложение долго держит экран списка объектов, память начинает расти очень быстро. Поэтому миграция на ARC в подобных проектах — не только про удобство разработки, но и про стабильность продукта на реальных устройствах. Я не раз видел, как после перехода на ARC количество обращений в поддержку из-за вылетов сокращалось на 30–40%, а среднее потребление памяти на ключевых экранах снижалось на 15–20%.
Мини-чеклист после миграции
- Нет вызовов
retain,release,autorelease. - Все свойства приведены к
strong/weak/copy. deallocоставлен только там, где действительно нужен.- Все bridging-места проверены вручную — особенно в модулях работы с файлами и графикой.
- Делегаты и observers не создают циклы.
- Старые файлы без ARC помечены осознанно и задокументированы.
- Критические сценарии приложения протестированы на деаллокацию (открыли/закрыли экран 10 раз — память вернулась к исходному уровню).
Когда лучше не мигрировать «в лоб»
Иногда полная автоматическая миграция сразу — плохая идея. Если проект содержит:
- много самописного C/CF-кода — например, кастомный движок рендеринга планов этажей;
- старые категории с нестандартным поведением — переопределение
deallocв категориях, которые сложно отследить; - неочевидные ownership-паттерны — ручное управление временем жизни через
CFRetain/CFReleaseвперемешку с Objective-C; - плохо покрытые тестами критические участки — например, модуль проведения сделки, где ошибка может стоить бизнесу денег;
то лучше разбить переход на этапы. Это снижает риск, особенно в коммерческих приложениях, где простой или регрессия стоят дороже, чем дополнительный день на аудит. В одном проекте для девелопера мы потратили неделю на ручную проверку каждого файла перед миграцией, но зато после перехода не было ни одного критического инцидента.
Вывод
Миграция с ручного управления памятью на ARC — это технически простая, но архитектурно чувствительная задача. Автоматический конвертер снимает рутину, но ответственность за dealloc, bridging, weak-ссылки и циклы удержания остаётся на разработчике. Если пройтись по проекту поэтапно, проверить проблемные места и не надеяться на «магическую кнопку», переход обычно проходит безболезненно и даёт заметный выигрыш в стабильности кода. Для приложений в сфере недвижимости, где надёжность напрямую влияет на репутацию агентства или девелопера, это особенно важно.
FAQ
Нужно ли полностью переписывать проект под ARC?
Нет. Можно мигрировать поэтапно и даже оставлять отдельные файлы без ARC, если это оправдано — например, старый модуль интеграции с проприетарной CRM, который пока некем заменить.
Можно ли вызывать release в ARC-коде?
Нет. ARC запрещает ручные вызовы retain, release и autorelease. При попытке вызвать их компилятор выдаст ошибку.
Что делать с dealloc?
Оставить только cleanup-логику: observers, KVO, C-ресурсы, файловые дескрипторы. Ручной release и [super dealloc] не нужны. В проектах с виртуальными турами мы оставляем в dealloc очистку временных папок с кэшем 3D-моделей.
Почему после миграции растёт память?
Чаще всего причина в циклах удержания, неверном bridging или забытых strong-ссылках на delegate/block. Проверьте, не осталось ли у вас сильных ссылок на контроллеры из замыканий, которые выполняются асинхронно.
Как понять, что миграция прошла успешно?
Если приложение собирается без ARC-ошибок, ключевые экраны корректно деаллоцируются (можно добавить лог в dealloc и убедиться, что он вызывается), а профилирование не показывает новых утечек, переход можно считать успешным. Дополнительный признак — уменьшение жалоб пользователей на вылеты при длительной работе с каталогом объектов.
