Программирование SwiftARC и памятьSenior iOS-разработчик на Swift

Достаточно ли отменить долгоживущую Task, чтобы захваченный ею объект сразу стал доступен для ARC?

Достаточно ли отменить долгоживущую Task, чтобы захваченный ею объект сразу стал доступен для ARC?

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

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

Нет. Отмена Task только устанавливает для неё состояние отмены; она не принудительно прерывает выполнение и не освобождает захваченные значения. Объект, захваченный замыканием Task сильной ссылкой, станет доступен для ARC после завершения операции Task и освобождения её замыкания.

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

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

В модели concurrency Swift отмена является кооперативной: вызывающая сторона сообщает задаче о необходимости остановиться, а сама задача должна проверить это состояние или вызвать отменяемую операцию.

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

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

Ошибочное предположение состоит в том, что вызов отмены немедленно уничтожает Task и все её захваты. Если тело задачи игнорирует отмену, оно может продолжать работу, а объект — оставаться живым неопределённо долго. Это приводит к утечкам ресурсов, неожиданному выполнению методов и удержанию крупных графов объектов.

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

При создании Task её замыкание сохраняет захваченные значения. Сильный захват экземпляра увеличивает число владельцев объекта; после обнуления внешней ссылки владельцем остаётся выполняемая Task.

Вызов отмены меняет флаг состояния Task. Он не прекращает поток, не разрушает стек вызовов и не удаляет замыкание. Освобождение захваченного объекта произойдёт только после фактического завершения Task и разрушения её контекста выполнения.

Корректная задача должна регулярно проверять отмену или использовать отменяемые операции:

import Foundation final class Screen { deinit { print("deinit") } } var screen: Screen? = Screen() let task = Task { [screen] in while !Task.isCancelled { try? await Task.sleep(for: .milliseconds(100)) } _ = screen } screen = nil _task.cancel()

В примере в последней строке должна использоваться переменная task, а не _task; после выхода из цикла Task освободит свой захват screen, и объект сможет быть уничтожен. Сам вызов cancel() лишь инициирует этот путь, но не заменяет проверку отмены.

Если задача должна переживать объект, применяют слабый захват. Однако слабый захват сам по себе не гарантирует отсутствие сильного захвата: например, временное присваивание объекта в локальную сильную переменную внутри итерации может удерживать его до конца этой итерации или дольше, если значение передано другой операции.

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

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

Экран запускает Task, которая периодически обновляет данные. При закрытии экрана разработчик обнуляет его внешнюю ссылку и вызывает cancel() у задачи, но deinit не вызывается.

Вариант с сильным захватом прост: Task гарантированно сохраняет экран живым до завершения работы. Минус — ошибка в цикле или игнорирование отмены удерживает экран и связанные ресурсы.

Вариант со слабым захватом не удерживает экран, но требует безопасной обработки исчезновения объекта. Если задача должна остановиться вместе с экраном, обычно используют слабый захват, проверку отмены в цикле и явную отмену задачи при завершении жизненного цикла экрана.

Выбранное решение должно обеспечивать оба условия: задача действительно реагирует на отмену, а сильные захваты не образуют лишний цикл. Тогда после завершения Task её контекст освобождается, и ARC уничтожает экран, если других сильных владельцев нет.

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

  1. Освободит ли отмена Task объект, если задача находится внутри await?

Нет, не обязательно. Ожидаемая операция должна поддерживать отмену и завершиться или бросить ошибку после установки флага отмены. Пока Task фактически не завершилась, её контекст и захваты могут сохраняться.

  1. Почему слабый захват не гарантирует немедленное уничтожение объекта внутри тела Task?

Чтение слабой ссылки обычно даёт optional-значение. Если оно временно связывается с сильной локальной переменной, эта переменная удерживает объект на время своего жизненного интервала. Кроме того, объект могут удерживать другие операции, свойства или вложенные замыкания.

  1. Достаточно ли отменить Task, если объект хранит сам дескриптор этой Task?

Не всегда. Нужно проверить полный граф владения: объект может удерживаться замыканием Task, а Task или связанная инфраструктура — объектом через сохранённый дескриптор либо callback. Обычно дескриптор отменяют и освобождают, а тело Task проектируют так, чтобы оно гарантированно вышло из работы; иначе отмена только меняет состояние, но не устраняет удержание объекта.