Объект класса хранит замыкание, которое обращается к этому же объекту: каким образом возникает цикл сильных...

Объект класса хранит замыкание, которое обращается к этому же объекту: каким образом возникает цикл сильных ссылок и как его разорвать?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Цикл возникает, когда объект сильно владеет замыканием через своё свойство, а замыкание по умолчанию сильно захватывает тот же объект. Счётчик ссылок объекта не достигает нуля, поэтому ARC не освобождает его. Обычно цикл разрывают захватом self как weak или, при доказанно более долгом времени жизни объекта, как unowned.

Исторический контекст

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

Замыкания сделали такую ситуацию особенно распространённой, потому что они хранят захваченные значения в своём контексте. Если замыкание сохраняется внутри объекта, захват self может незаметно создать обратную сильную ссылку.

Постановка проблемы

Пусть объект хранит обработчик события, таймер или callback. Если обработчик захватывает self по умолчанию, образуется цепочка: объект сильно владеет замыканием, замыкание сильно владеет объектом.

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

Подробное решение

Захват weak не увеличивает счётчик ссылок. Такая ссылка автоматически становится nil после освобождения объекта, поэтому замыкание должно обращаться к объекту через Optional.

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

final class Screen { var onRefresh: (() -> Void)? init() { onRefresh = nil onRefresh = { [weak self] in self?.reload() } } func reload() {} }

Здесь Screen владеет замыканием, но замыкание не владеет Screen. Когда исчезает последняя внешняя сильная ссылка, объект освобождается, а self внутри замыкания становится nil.

Другой вариант — явно обнулить callback до уничтожения владельца. Это подходит, если callback должен владеть объектом во время работы, но требует дисциплины и не всегда защищает от сложных цепочек. weak безопаснее при неопределённом времени жизни, а unowned не требует Optional-проверок, но цена ошибки — runtime crash.

Ситуация из практики

Экран сохраняет callback обновления в объекте таймера. Callback использует self, чтобы обновить интерфейс, а таймер живёт дольше экрана. При обычном захвате возникает цикл: экран удерживает таймер или его callback, callback удерживает экран.

Вариант с сильным захватом прост, но создаёт утечку. Вариант с unowned экономит Optional-проверку, однако опасен: таймер может вызвать callback после закрытия экрана. Выбранный вариант — [weak self] с безопасным обращением через self?.: экран освобождается вовремя, а поздний callback ничего не делает.

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

Что кандидаты часто упускают

  1. Достаточно ли пометить self как weak, чтобы гарантированно устранить утечку?

Нет. Нужно проверить всю цепочку владения. Замыкание может захватывать другой объект, который сильно удерживает исходный объект, либо несколько callbacks могут образовать цикл без непосредственного захвата self. weak устраняет конкретную сильную ссылку, но не анализирует остальные связи.

  1. Почему захват локальной переменной может отличаться от захвата self?

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

  1. Когда unowned предпочтительнее weak?

unowned подходит, когда жизненный цикл захватываемого объекта строго длиннее или равен жизненному циклу замыкания, и это гарантируется архитектурой. Например, дочерний объект может безопасно ссылаться на владельца, если дочерний объект не может существовать после владельца. При сомнении следует использовать weak: он безопаснее, хотя требует обработки возможного nil.