Какая особенность подсчёта ссылок мешает ARC автоматически освобождать недостижимый цикл сильных ссылок?
ARC не освобождает недостижимый цикл, потому что каждый объект внутри него всё ещё имеет хотя бы одну сильную входящую ссылку. Счётчик сильных ссылок не становится равным нулю, поэтому ARC считает объекты живыми, даже если внешние ссылки на цикл исчезли.
ARC автоматизировал управление сильными ссылками, заменив ручные операции удержания и освобождения объектов. Такой подход решает большинство обычных проблем времени жизни, но сохраняет фундаментальное ограничение подсчёта ссылок: он учитывает количество владельцев, а не достижимость объекта из корня программы.
В отличие от трассирующих сборщиков мусора, ARC не обязан периодически обходить весь граф объектов в поисках недостижимых компонентов. Поэтому циклы владения требуют явного разрыва одной из ссылок.
Рассмотрим два объекта, которые сильно ссылаются друг на друга. После удаления внешних ссылок они становятся недостижимыми для приложения, но взаимные ссылки продолжают удерживать каждый объект.
В этом случае deinit не вызывается: у каждого экземпляра остаётся сильная ссылка из другого экземпляра. Последствие — утечка памяти до завершения процесса или до другого внешнего механизма, который разорвёт цикл.
ARC поддерживает учёт сильных ссылок. При создании сильной ссылки условный счётчик владения увеличивается, а при её уничтожении уменьшается. Когда сильных владельцев не остаётся, объект освобождается, после чего освобождение его свойств может запустить каскадное уничтожение других объектов.
В цикле это каскадное освобождение не начинается: каждый объект удерживается соседним объектом. ARC не делает вывод «оба объекта недостижимы извне», поскольку стандартный подсчёт ссылок не хранит и не анализирует полную структуру графа владения.
Чтобы разорвать цикл, одну связь нужно сделать не владеющей. Обычно для обратной связи используют weak, если объект может исчезнуть раньше ссылки. unowned подходит только при доказанном порядке времени жизни: обращение после уничтожения объекта завершится ошибкой выполнения.
Выбор weak безопаснее, но требует optional-доступа: после уничтожения объекта ссылка автоматически станет nil. unowned не добавляет дополнительной проверки на каждом обращении и выражает более сильный инвариант, однако ошибка в предположении о времени жизни приводит к аварийному завершению.
Важно, что не каждый граф с несколькими ссылками является циклом. Цикл возникает именно тогда, когда существует замкнутый путь из сильных ссылок, например A → B → A. Одной не владеющей ссылки на таком пути достаточно, чтобы убрать взаимное удержание.
В модели экрана контроллер сильно владеет моделью данных, а модель хранит callback для уведомления контроллера. Если callback сильно захватывает контроллер, возникает цикл: контроллер владеет моделью, модель владеет callback, callback владеет контроллером.
Варианты решения:
weak — безопасно при независимом завершении экрана, но callback может ничего не сделать, если контроллер уже уничтожен;unowned — не требует optional-обработки, но корректен только при гарантии, что callback никогда не будет вызван после уничтожения контроллера;Для обычного асинхронного callback был бы выбран weak-захват: операция может завершиться после ухода экрана, поэтому callback должен безопасно стать бездействующим. Результат — контроллер и связанная модель освобождаются после исчезновения внешних владельцев, а позднее событие не вызывает обращение к уничтоженному объекту.
Да. Для освобождения цикла достаточно убрать владение хотя бы на одном ребре замкнутого пути. После исчезновения внешних сильных ссылок объекты больше не удерживают друг друга полностью: один из них может быть уничтожен, затем его сильные свойства освобождаются, и процесс продолжается.
Это не означает, что любую ссылку можно бездумно заменить на weak. Нужно сохранить требуемую семантику владения: если объект должен гарантированно удерживаться владельцем, слабая ссылка на нём может привести к преждевременному исчезновению.
Нет. Размер цикла значения не имеет. Если существует замкнутый путь сильных ссылок, например A → B → C → A, каждый объект продолжает иметь сильного владельца, поэтому обычное уменьшение внешних ссылок не приводит к освобождению.
Разрыв любого одного ребра цикла устраняет взаимное удержание, но конкретный выбор зависит от модели владения. Обычно слабой делают обратную или делегатную связь, а не произвольное ребро.
Да. Участником цикла может быть замыкание, хранящееся в свойстве, коллекция, callback очереди или другой объект-посредник. Например, объект может сильно хранить замыкание, а замыкание — сильно захватывать исходный объект.
Поэтому при анализе утечки нужно искать не только пары свойств owner и child, но и все долгоживущие замыкания и обратные callbacks. Если замыкание не должно владеть объектом, применяют список захвата с weak или unowned, выбирая вариант по гарантии времени жизни.