У владельца сохранён таймер, чей callback захватывает этого владельца. Как разорвать цикл владения, не удер...

У владельца сохранён таймер, чей callback захватывает этого владельца. Как разорвать цикл владения, не удерживая владельца таймером?

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

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

Callback должен захватывать владельца через weak, а не через сильную ссылку. Тогда цепочка «владелец → таймер → callback» не замыкается обратно на владельца, и ARC сможет уничтожить его после освобождения остальных сильных ссылок.

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

ARC автоматизировал управление сильными ссылками, избавив Swift-код от ручных операций retain и release. Однако ARC работает с подсчётом владения и не может самостоятельно определить, что замкнутый цикл больше не нужен.

Для связей, которые не должны управлять временем жизни объекта, появились специальные виды ссылок: weak и unowned. Они позволяют описать не-владеющие отношения и тем самым разорвать циклы.

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

Владелец хранит таймер, таймер хранит callback, а callback сильно захватывает владельца. Даже после удаления внешней ссылки на владельца остаётся цикл сильных ссылок, поэтому его deinit не вызывается.

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

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

Callback должен хранить weak-ссылку на владельца. При выполнении callback слабая ссылка либо временно загружается как сильная на время операции, либо оказывается nil, если владелец уже уничтожен.

После уничтожения владельца таймер может продолжить существовать, например потому, что его удерживает run loop. Поэтому жизненный цикл таймера нужно завершать отдельно: при закрытии экрана или в другом явно определённом месте следует остановить или инвалидировать таймер.

Выбор между ссылками определяется гарантией времени жизни:

  • weak подходит, если объект может исчезнуть раньше callback; обращение после уничтожения даёт nil;
  • unowned допустим только при доказанной гарантии, что callback не переживёт владельца; нарушение гарантии вызывает ошибку выполнения;
  • сильный захват сохраняет владельца, поэтому в такой цепочке обычно создаёт retain cycle.

weak не является механизмом синхронизации между потоками. Он лишь не участвует во владении и автоматически обнуляется при уничтожении объекта. Конкурентный доступ к состоянию таймера и владельца всё равно требует отдельной синхронизации.

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

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

Первый вариант — оставить сильный захват и полагаться на остановку таймера в обычном методе закрытия. Его недостаток — при забытом или не выполненном пути закрытия цикл сохраняется.

Второй вариант — захватить экран через unowned. Цикл исчезает, но поздний callback может обратиться к уничтоженному объекту и аварийно завершить приложение.

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

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

  1. Достаточно ли заменить сильный захват на weak, чтобы таймер перестал работать?

Нет. weak разрывает владение владельцем, но не управляет временем жизни самого таймера. Если таймер удерживается run loop или другим объектом, он может продолжать срабатывать; callback будет получать nil вместо владельца. Таймер нужно явно остановить или инвалидировать.

  1. Почему unowned не является более подходящей оптимизацией для любого callback?

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

  1. Может ли deinit владельца сам по себе быть гарантированным способом остановить таймер?

Только если владелец действительно достигнет deinit и код корректно остановит таймер. При сильном захвате callback владелец до deinit не дойдёт из-за цикла, поэтому такой cleanup не будет вызван. Сначала нужно устранить цикл через не-владеющий захват, а затем явно оформить остановку таймера в подходящей точке жизненного цикла.