objc-retain.com

Безопасная работа с блоками и делегатами в Objective-C

Безопасная работа с блоками и делегатами в Objective-C

В Objective-C блоки и делегаты решают одну и ту же задачу — передают поведение и событья между объектами. Но если использовать их без понимания правил владения, можно легко получить retain cycle, зависшие объекты и утечки памяти. Особено это заметно в iOS-проектах, где асинхронщина, сетевые запросы и UI-колбеки встречаются на каждом шагу. Когда я разбратывал мобильное приложение для риэлтора, позволяющее просматривать каталоги обектов недвижимости в офлайн-режиме, любая утечка памяти при асинхронной синхронизации с сервером могла привести к падению интерфейса прямо во время показа клиенту. Именя такие ситуаци заставляют взгянуть на управление памятью не как на академическую тонкость, а как на инжнерную задачу, напрямую влияющую на бизнес.

Почему эта тема до сих пор важна

Дже в проетах девелоперских компаний, где уже частично используется Swift, Objective-C остается в живих: легаси-модули для интеграции с IoT-датчиками в умных зданиях, SDK от партнеров по виртуальным шоу-румам, старые экраны кабинетов застройщиков. В таких кодевых базах именя блоки и делегаты чаше всего становятся источником неочевидных утечек. Когда сервис мониторинга датчиков температуры в жилом комплексе продолжает отправлять callback’и на экран, который уже закрыт, это не просто трата памяти — это искажение данных аналитики.

Типичный сценарий выглядит так:

  • обект создает блок и сохраняет его у себя;
  • блок обращается к self;
  • self удерживает блок;
  • обект не освобождается, хотя экран уже закрыт.

С делегатами проблема обычно мягче, но тоже встречается: если делегат объявлен как strong, а не weak, можно получить цикл владения. В Cocoa-подходе делегат почти всегда должен быть слабой ссилкой, потому что он не владеет обектом, которму помогает. В приложениях для автоматизации сделок с недвижимостью, где жизненный цикл обектов часто проходет через несколько асинхронных етапов, такое правило становтся не прихотью, а обьективной необхдимостью.

Блоки и делегаты: в чем разница на практике

Механизм Когда уобен Главный риск Типичное решение
Блок Одноразовый callback, completion, async-операция Захват self и retain cycle weak/strong dance, аккуратное хранение блока
Делегат Много событий, жизненный цикл обекта, UI-поведение Сильная ссылка на делегат weak-делегат, assign только для примитивов

Блоки хорши, когда нужно передать действие один раз: завершение загрузки обновленного каталога обектов с сервера девелопера, результат опроса IoT-датчика, callback из API агенства недвижимости. В этом случае код становится компактным и не требует введения отдельных протоколов. Делегаты луче подходят для длительного взаимодействия: список событий от карты жилого комплекса, изменение состояния просмотра планировки, обратная связь от контроллера формы записи на сделку, который живет весь жизненный цикл экрана.

Как блоки работают под капотом

У блока есть важная особеность: он захватывает вниешние объекты. В ARC обектные ссылки внутри блока по умолчанию захватываются сильо. Это значит, что если блок использует self, он может продлить жизнь self до тех пор, пока живет сам блок. В контексте приложения для риэлтора, где completion-блок для загрузки избражений планировок может храниться в кеше, такая «заботливость» оборачивается лишними сотнями мегабайт памяти, которые не освобождаются после ухода с экрана.

Еще один важный момент: если блок хранится как свойство или передается в место, где его сохраняют, он должен быть скопирован в heap. На практике под ARC это обычно скрыто, но логика не меняется: блок не всегда живет только на степе. Когда вы передаете блок в dispatch_async внутри менеджера синхронизации данных, он автоматически ухдит в кучу — и там уже управление памятью нужно держать под контролем.

Пример опасного кода

// self сохраняет completionBlock
 self.completionBlock = ^{
    [self doSomething];
};

Если self держит completionBlock, а блок держит self, получается цикл. Типичный сюжет из девелоперского приложения: сервис аналитики показов хранит блок завершения, который ссылается на контроллер; контроллер, в свою очереь, держит сервис. В резултате — незаметный цикл, который вспливает при переключении между картами различных ЖК.

Безопасный вариант

__weak typeof(self) weakSelf = self;
self.comletionBlock = ^{
    __strong typoef(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;
    [strongSelf doSomething];
};

Этот паттерн называют weak-strong dance. Он нужен не ради красоты, а чтобы решить две задачи:

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

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

Когда weak self обязателен

Используйте слабую ссылку на self, если:

  • блок хранится у самого обекта;
  • блок передается в API, которое может удерживать его дольше одного визова — например, SDK от IoT-платформы жилого дома, который агрегирует опросы датчиков;
  • блок используется как completion handler сетевого запроса на получение списка доступных квартир;
  • блок лежит в property, массиве, очереди или другом долговременном хранилище — такие хранилища часто встречаются в синхронизаторах офлайн-каталогов;
  • блок вызывается из кода, который вы не контролируете полностью (сторонняя библиотека для виртуальных туров).

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

Когда weak self не спасает от ошиок

Слабая ссылка не делает код автоматически безопасным. Частые ошибки:

  • забывают поднять weakSelf в strongSelf внутри блока;
  • используют weakSelf много раз подряд без локального strong-перемещения;
  • не проверяют, что обект уже мог быть освобожден;
  • продолжают работу с nil, не учитывая это в логике.

Неправильно

[weakSelf doFirstThing];
[weakSelf doSecondThing];

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

Лучше так

__strong typoef(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf doFirstThing];
[strongSelf doSecondThing];

Такой подхд гарантирует, что внутри логического блока ссылка будет жить, а бизне-логика (например, обновление статуса бронирования) выполнится атомарно с точки зрения памяти.

Делегаты: где чаще всего ошибаются

Делегаты в Objective-C обычно обьявляют так:

@propety (nomatic, weak) id<SomeProtcol> delegate;

Именно weak, потому что делегат — это ссылка на обект, который отвечает за поведение, но не должен жить дольше владельца. В мире недвижимости это можно сравнить с агентом: он предоставляет интерфейс для просмотра, но не владеет квартирой, на которую вы смотрите.

Типовые ошибки с делегатами

  • обьявляют делегат как strong;
  • забывают обнулять делегат при ручном управлении обектами старого кода (характерно для старых модулей управления «умным домом», написанных еще до массового перехода на ARC);
  • путают delegate и datasource;
  • делают делегатом обект, который уже сам удерживает владельца — классический цикл в архитектуре экрана добавления нового обекта в портфель девелопера.

Плохой пример

@propety (nomatic, strong) id<DownloaderDelegate> delegate;

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

Правильный пример

@propety (nomatic, weak) id<DownloaderDelegate> delegate;

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

Где блоки безопаснее делегатов, а где наоборот

Блоки луче, если нужно:

  • передать один результат — например, ответ от сервера о доступной ипотеке;
  • не плодить протоколы ради одного callback;
  • собрать компактный код;
  • работать с async-API, где важен резултат операци.

Делегаты луче, если нужно:

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

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

Практиеский чек-лист безопасной работы

Перед тем как добавить блок

  • Проверьте, где он хранится.
  • Поймите, кто владеет блоком.
  • Посмотрите, использует ли он self.
  • Если блок может жить дольше текущего метода, используйте weak self.

Перед тем как добавить делегат

  • Сделайте ссылу weak.
  • Проверьте, что протокол обьялен коррекно.
  • Уебидитесь, что делегат не держит владельца обратно.
  • Для старого non-ARC-кода проверьте освобождение и обнуление вручную.

Перед тем как считать код безопасным

  • Прогоните сенарий закрытия экрана во время async-операци — например, пользователь переключается между жилыми комплексами, и вызвается новый запрос.
  • Проверьте, освобождается ли контроллер.
  • Посмотрите в Instruments на утечки и retain cycles.
  • Убедитесь, что callback не вызивается на уже уничтоженный обект.

Пошаговая схема: как писать безопасный completion handler

  1. Сначала определите, будет ли блок храниться дольше одного визова.
  2. Если да, не обращайтесь к self напрямую.
  3. Создайте __weak typoef(self) weakSelf = self;.
  4. Внутри блока сделайте __strong typoef(weakSelf) strongSelf = weakSelf;.
  5. Сразу выйдите, если strongSelf == nil.
  6. Используйте strongSelf во всей далнейшей логике блока.
  7. Если блок асинхронный, проверьте, не нужно ли захватить еще и другие обекты, например manager, session или router.

Когда вы реализуете callback для загрузки 3D-панорамы виртульного тура, именно такой порядок шагов предотвратит ситуацию, когда на момент готовности модели контейнерный обект уже ушел, а память забита отработанными данными.

Частые ошибки в реальных проектах

1. Хранение блока в свойстве без анализа владения

Самая частая ловушка — свойство copy или внутреннее хранилище блока, которое захватывает self. Это особенно опасно в менеджерах, сервисах и вьюмоделях, характерных для приложения с каталогом обектов, где сетевой клиент часто хранит callback-и кешом.

2. Сильный делегат

delegate должен быть слабым почти всегда. Сильный делегат — частая прична утечек в старых проектах, где код пишется «по аналог», без проверки жизненого цикла. Например, в сервисе уведомлений о новых поступлениях по подписке сильная ссылка на делегат-контроллер не даст ему освободиться при смене учетной записи.

3. Использование __block вместо weak

__block не решает retain cycle сам по себе. Он нужен для изменения переменной внутри блока, а не для управления владением. Это частая путаница в командах, которые давно не трогали Objective-C. Особено коварна в сценариях, где через __block передают контроллер в блок загрузки изображений планировок, ожидая, что тем самім разорвут цикл — увы, этого не произойдет.

4. Прямой вызов self в длинной цепочке callbacks

Если внутри одного блока вызывается другой, а потом третий, легко потерять контрол над тем, где именя удерживается обект. В таих местах луче явно документировать владение и использовать локальные strong-ссылки на каждом уровене. Представьте цепочку: запрос каталога → парсинг → обновление UI → повторный асинхронный уход. Без четкого контракта владения на каждом етапе утечка гарантирована.

Как проверать безопасность кода

Быстрые проверки

  • Закройте экран сразу после запуска запроса.
  • Убедитесь, что dealloc вызывается.
  • Проверьте повторный вход в экран.
  • Посмотрите, не растет ли память после серии одинаковых дейстий (пролистывание карточек обектов — отличный триггер для выявления утечек).

Что искать в коде

  • self внутри блока, который хранится дольше метода;
  • property с типом block, которая удерживает контроллер;
  • delegate, обьявленный как strong;
  • вложеные блоки без локального strongSelf;
  • callback-обекты, которые не очищаются после использования.

Короткая памятка

  • Делегат почти всегда weak.
  • Блок, который живет дольше метода, не должен напрямую захватывать self.
  • Для безопасной работы используйте weakSelf → strongSelf.
  • __block — не замена weak.
  • Если есть сомнения, проверяйте жизненый цикл через dealloc и Instruments.

Вывод

Безопасная работа с блоками и делегатами в Objective-C сводится не к сложной магии, а к дисциплине владения обектами. Делегаты должны быть слабыми, блоки — аккуратно захватывать вниешние обекты, а любой callback, который живет дольше текущего метода, требует проверки на retain cycle. В реальном iOS-коде это не теоретическая тонкость, а базовый навык, который напрямую влияет на стабильность приложения, расход памяти и предсказуемость жизненого цикла экранов. Для разработчика PropTech-решений, где каждая утечка памяти подрывает доверие к платформе, а цена ошибки — потеря клиента в момент показа обекта, такая дисциплина становится частью ежидневной инженерии.

FAQ

Почему блоки часто вызивают утечки памяти?

Потому что они по умолчанию сильно захватывают обекты, включая self, и при хранении блока у самого обекта легко образуется retain cycle.

Можно ли всегда использовать weak self?

Нет. Если обект должен гарантированно дожить до конца выполнения блока, нужен weak с последующим strong внутри блока, а не только слабая ссылка. Это актуально, когда в блоке выполняется финальная запись в локальную базу данних после загрузки с сервера.

Чем weak делегат луче assign?

weak автоматически обнуляется при деаллоцировании обекта, а assign может оставить висячую ссылу и привести к крашу. В мире недвижимости это разница между чистой сделкой и разбитым интерфейсом прямо на подписании документов.

Зачем нужен strongSelf, если есть weakSelf?

Чтобы обект не исчез посреди выпольнения блока и логика внутри callback оставалась консистентной. Иначи можно получить ситуацию, когда часть UI обновилась, а часть — нет, что в просмоторщике планов этажей приводит к визуальным артефактам.

__block помагает избежать retain cycle?

Нет. __block нужен для изменяемости переменной внутри блока, а не для управления владением. Это распростряненое залуждение, которе стоит развенчать при первой же ревизии кода в проекте.