Вложенное escaping-замыкание обращается к self внутри внешнего замыкания с [weak self]. Что определяет, станет ли self сильным захватом во внутреннем замыкании?
Список захвата действует только на то замыкание, в котором он объявлен. Поэтому [weak self] во внешнем замыкании не делает автоматически слабым захват во внутреннем: если внутреннее escaping-замыкание использует self без собственного списка захвата, оно обычно удерживает self сильной ссылкой.
ARC автоматизировал подсчёт ссылок, но не стал анализировать намерения разработчика в каждом замыкании. Замыкания могут переживать функцию и сохранять захваченные значения, поэтому для управления временем жизни объекта Swift использует явные списки захвата: weak, unowned или сильный захват по умолчанию.
Такой подход сохраняет предсказуемость: каждое замыкание самостоятельно описывает, как оно владеет захваченными объектами. Автоматическое наследование weak-семантики во вложенных замыканиях могло бы неожиданно приводить к обращению к уже уничтоженному объекту.
Внешнее замыкание может быть безопасным для владельца: оно захватывает self слабо и не образует цикл. Но при создании внутри него другого escaping-замыкания компилятор формирует отдельный контекст захвата.
Если внутреннее замыкание сохраняется, например как callback сетевого запроса, его сильный захват self способен продлить жизнь объекта на неопределённый срок или создать цикл: объект хранит callback, а callback хранит объект.
Каждое замыкание имеет собственный набор захваченных значений. Внешнее замыкание хранит weak-ссылку, но внутреннее замыкание, использующее self, должно получить ссылку для будущих вызовов. Без собственного [weak self] такая ссылка является сильной.
После выполнения outer внутреннее замыкание сохраняется в saved. Оно захватывает self сильно, поэтому обнуление внешних ссылок на Controller не обязательно приведёт к его уничтожению. Если saved принадлежит самому Controller, возникает цикл Controller → saved → внутреннее замыкание → Controller.
Безопасный вариант — явно задать список захвата для каждого escaping-замыкания: [weak self] во внутреннем callback тоже. Тогда callback не владеет Controller, а при вызове после его уничтожения self будет nil. Компромисс состоит в том, что callback может ничего не выполнить; если операция обязана завершиться, нужно отдельно управлять её отменой или временем жизни владельца.
Сильная локальная привязка через guard let self во внешнем замыкании также важна: она удерживает объект до конца выполнения внешнего замыкания. Если внутреннее замыкание захватывает эту локальную сильную привязку, оно дополнительно продлевает жизнь объекта.
Экран хранит callback завершения загрузки, а загрузчик сохраняет переданный callback до ответа сервера. Разработчик добавляет [weak self] только во внешнее замыкание, полагая, что контроллер не будет удерживаться. В результате внутренний callback получает сильный захват self, а контроллер, хранящий внешний callback, может оказаться частью цикла владения.
Вариант с сильным захватом прост и гарантирует доступ к контроллеру, но может удерживать экран после закрытия. Вариант со слабым захватом только внешнего замыкания выглядит безопаснее, однако не защищает от сильного захвата во внутреннем callback.
Правильное решение — явно указать слабый захват во внутреннем escaping-замыкании и отменять загрузку при уничтожении экрана, если это поддерживает используемый загрузчик. В результате контроллер не удерживается callback-цепочкой, а завершившаяся позднее операция корректно становится no-op при отсутствии владельца.
Вопрос 1: Достаточно ли написать [weak self] во внешнем замыкании, если внутреннее замыкание не сохраняется и является non-escaping?
Нет, это не даёт внутреннему замыканию отдельной weak-семантики, но риск долгого удержания обычно отсутствует. Non-escaping-замыкание не может пережить вызов функции, поэтому его сильный захват действует только в пределах операции; после завершения вызова сохранённый им объект не удерживается.
Однако во время выполнения внутреннего замыкания объект может быть временно удержан. Это влияет на момент уничтожения, но не создаёт долгоживущий цикл само по себе.
Вопрос 2: Что изменится, если во внешнем замыкании выполнить guard let self?
После успешного guard let появляется сильная локальная ссылка на объект, действующая до конца текущего вызова внешнего замыкания. Если внутреннее escaping-замыкание использует эту локальную переменную self, оно может захватить её сильно и продлить жизнь объекта после завершения внешнего вызова.
Такой код уместен, когда операция должна гарантированно завершиться с живым владельцем. Для экранов и контроллеров это опасно: длительная операция или цикл callback может удерживать объект дольше ожидаемого.
Вопрос 3: Как проверить, что именно внутреннее замыкание удерживает объект?
Нужно временно разорвать или очистить все места, где сохраняются callback-и, затем проверить вызов deinit. Полезно также проследить граф владения в Memory Graph Debugger или Instruments: ищут путь от долгоживущего объекта-источника к callback, затем к его контексту захвата и Controller.
Замена захвата во внутреннем замыкании на weak служит проверкой гипотезы, но не заменяет анализ остальных владельцев. Если deinit после этого не вызывается, объект удерживается другим сильным путём или ещё выполняется операция, временно сохраняющая его.