objc-retain.com

Миграция проекта с ручного управления памятью на ARC

Миграция проекта с ручного управления памятью на ARC

Переход с 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:

  • снять NSNotificationCenter observer;
  • остановить 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 и убедиться, что он вызывается), а профилирование не показывает новых утечек, переход можно считать успешным. Дополнительный признак — уменьшение жалоб пользователей на вылеты при длительной работе с каталогом объектов.