Хранимый completion handler держит свой view controller живым — разберите владение и устраните утечку
View controller запускает работу через общий синглтон и передаёт completion handler. Обработчик сохраняется, но никогда не вызывается, а deinit контроллера не печатается — контроллер утекает.
Ограничения:
Downloader.shared— синглтон уровня процесса, который никогда не освобождается.- Сохраните API с completion handler; не удаляйте синглтон.
final class Downloader {
static let shared = Downloader()
private var onDone: (() -> Void)?
func start(_ completion: @escaping () -> Void) {
onDone = completion
}
}
final class ProfileVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Downloader.shared.start { self.render() }
}
func render() {}
deinit { print("ProfileVC gone") }
}
Разберите владение и устраните утечку.
Бессмертный синглтон Downloader.shared хранит onDone с сильным захватом view controller. Так как start его не вызывает и не очищает, замыкание удерживает контроллер, а deinit не срабатывает. Исправьте через [weak self] и onDone = nil после вызова.
- ✗Винят фоновый поток вместо сильного захвата, который держит синглтон
- ✗Думают, что контроллер удерживает синглтон, а не наоборот
- ✗Считают причиной сам
@escaping, а не так и не очищенное хранимое замыкание
- →Почему очистка
onDoneпосле вызова важна даже при[weak self]? - →Когда
[unowned self]неверен для обработчика, способного пережить контроллер?
Виновата не многопоточность, а цепочка владения. Downloader.shared — синглтон, который живёт всё время работы процесса. Он хранит onDone, а замыкание через self.render() сильно захватывает ProfileVC. Так как start кладёт замыкание в onDone, но никогда его не вызывает и не обнуляет, синглтон бессрочно удерживает замыкание, а через него — контроллер. Счётчик ссылок ProfileVC не падает до нуля, поэтому deinit не печатается.
func start(_ completion: @escaping () -> Void) {
onDone = completion
// …выполняем работу, затем:
onDone?()
onDone = nil // отпускаем хранимое замыкание
}
// место вызова:
Downloader.shared.start { [weak self] in self?.render() }
[weak self] убирает сильную ссылку замыкания на контроллер, а onDone = nil после вызова освобождает само замыкание в синглтоне. Достаточно любого из двух, чтобы разорвать удержание, но правильнее сделать оба: слабый захват защищает от переживания, а очистка не даёт синглтону копить мёртвые замыкания.