Как связаны retain cycle и escaping closure в Swift?
Что проверяют
Понимаете ли вы, что escaping-замыкание само по себе не утечка: цикл возникает, только когда объект, владеющий замыканием, захвачен им сильно. Следом обычно просят показать цикл на реальном примере и объяснить выбор между weak и unowned.
Как отвечать
Сначала механика. Замыкание это ссылочный тип, и по умолчанию оно захватывает объекты сильно. Параметр-замыкание в Swift по умолчанию non-escaping: его нельзя сохранить, оно отработает до выхода из функции и цикла не создаст. @escaping значит, что замыкание переживёт вызов: его кладут в свойство, в очередь, в подписку. Поэтому внутри escaping-замыканий у классов компилятор требует явный self.: захват должен быть виден.
Цикл выглядит так: self владеет замыканием (свойство-колбэк, подписка в cancellables), а замыкание сильно держит self. Счётчик ссылок не опускается до нуля, deinit не вызывается.
Типичные места в iOS-коде:
- колбэки в свойствах:
viewModel.onUpdate = { self.render() }, когдаselfдержитviewModel; - Combine:
sinkс сильнымself, сохранённый вcancellablesтого же объекта, иassign(to:on:)сselfв роли объекта; - таймеры:
Timer.scheduledTimerс блоком: RunLoop держит таймер, таймер держит замыкание, пока не вызванinvalidate(); Taskв Swift Concurrency: задача держитselfдо завершения, и бесконечныйfor awaitвнутри неё не даст объекту умереть.
Решение: [weak self] и guard let self else { return }, либо [unowned self], если объект гарантированно живёт дольше замыкания. Иногда проще захватить только нужное: [service]. Утечку видно в Memory Graph Debugger в Xcode.
Где спотыкаются
Считают, что любое escaping-замыкание с self течёт. DispatchQueue.main.async { self.update() } держит объект только до выполнения блока, цикла здесь нет.
Ставят unowned в асинхронных сетевых колбэках. Если экран закрыли раньше ответа, обращение к освобождённому объекту уронит приложение.
Пишут [weak self] только во вложенном замыкании. Внешнее escaping-замыкание при этом захватывает self сильно, чтобы передать его внутрь, и цикл остаётся.