У владельца сохранён таймер, чей callback захватывает этого владельца. Как разорвать цикл владения, не удерживая владельца таймером?
Callback должен захватывать владельца через weak, а не через сильную ссылку. Тогда цепочка «владелец → таймер → callback» не замыкается обратно на владельца, и ARC сможет уничтожить его после освобождения остальных сильных ссылок.
ARC автоматизировал управление сильными ссылками, избавив Swift-код от ручных операций retain и release. Однако ARC работает с подсчётом владения и не может самостоятельно определить, что замкнутый цикл больше не нужен.
Для связей, которые не должны управлять временем жизни объекта, появились специальные виды ссылок: weak и unowned. Они позволяют описать не-владеющие отношения и тем самым разорвать циклы.
Владелец хранит таймер, таймер хранит callback, а callback сильно захватывает владельца. Даже после удаления внешней ссылки на владельца остаётся цикл сильных ссылок, поэтому его deinit не вызывается.
Использование unowned здесь опасно: таймер или callback могут обратиться к владельцу после его уничтожения. Это приведёт к аварийному завершению программы. Простая замена ссылки на weak разрывает цикл, но сама по себе не останавливает таймер.
Callback должен хранить weak-ссылку на владельца. При выполнении callback слабая ссылка либо временно загружается как сильная на время операции, либо оказывается nil, если владелец уже уничтожен.
После уничтожения владельца таймер может продолжить существовать, например потому, что его удерживает run loop. Поэтому жизненный цикл таймера нужно завершать отдельно: при закрытии экрана или в другом явно определённом месте следует остановить или инвалидировать таймер.
Выбор между ссылками определяется гарантией времени жизни:
nil;weak не является механизмом синхронизации между потоками. Он лишь не участвует во владении и автоматически обнуляется при уничтожении объекта. Конкурентный доступ к состоянию таймера и владельца всё равно требует отдельной синхронизации.
Экран создаёт периодический таймер и сохраняет его в свойстве. Сильный захват экрана в callback приводит к циклу: экран удерживает таймер, таймер удерживает callback, callback удерживает экран. После закрытия экрана интерфейс исчезает, но экземпляр остаётся в памяти.
Первый вариант — оставить сильный захват и полагаться на остановку таймера в обычном методе закрытия. Его недостаток — при забытом или не выполненном пути закрытия цикл сохраняется.
Второй вариант — захватить экран через unowned. Цикл исчезает, но поздний callback может обратиться к уничтоженному объекту и аварийно завершить приложение.
Выбранное решение — weak-захват плюс явная остановка таймера в жизненном цикле экрана. Экран может быть уничтожен независимо от таймера, а таймер не продолжает выполнять бесполезные callbacks после завершения работы экрана.
Нет. weak разрывает владение владельцем, но не управляет временем жизни самого таймера. Если таймер удерживается run loop или другим объектом, он может продолжать срабатывать; callback будет получать nil вместо владельца. Таймер нужно явно остановить или инвалидировать.
unowned не обнуляется и не проверяет наличие объекта так, как weak. Она допустима только при строгой гарантии, что callback никогда не выполнится после уничтожения владельца. Если таймер, очередь или внешний менеджер могут задержать callback, эта гарантия нарушается, и обращение по unowned приводит к runtime-ошибке.
Только если владелец действительно достигнет deinit и код корректно остановит таймер. При сильном захвате callback владелец до deinit не дойдёт из-за цикла, поэтому такой cleanup не будет вызван. Сначала нужно устранить цикл через не-владеющий захват, а затем явно оформить остановку таймера в подходящей точке жизненного цикла.