Retain cycle - одна из самых коварных проблем в
Swift. Даже опытные разработчики могут не заметить, как создают цикл удержания ссылок, который мешает освобождению памяти. Разберем пример, который выглядит безобидно, но содержит скрытую угрозу.
Что такое Retain cycle:
Retain Cycle (цикл удержания сильных ссылок) в
Swift - это ситуация, когда два или более объекта удерживают друг друга через сильные ссылки (strong), не позволяя
ARC (Automatic Reference Counting) освободить память. Это приводит к утечкам памяти, так как объекты никогда не освобождаются.
Проблемный код:
class MyClass {
var task: Task?
init() {
task = Task { [weak self] in
guard let self else { return }
repeat {
self.performSomeWork() // <- Цикл удержания!
} while !Task.isCancelled
}
}
func performSomeWork() {
}
deinit {
print("deinit") // Не выполнится
task?.cancel()
}
}
Что не так:
- guard let self создает сильную ссылку на весь блок Task.
- Цикл repeat работает бесконечно, удерживая self -> deinit никогда не вызовется.
Как исправить:
Изменить область видимости сильной ссылки:
Перенесите
guard внутрь цикла, чтобы ссылка освобождалась после каждой итерации:
task = Task { [weak self] in
repeat {
guard let self else { return }
self.performSomeWork() // Сильная ссылка только здесь
} while !Task.isCancelled
}
Использовать слабую ссылку везде:
Если метод не требует обязательного
self:
task = Task { [weak self] in
repeat {
self?.performSomeWork() // слабая ссылка
} while !Task.isCancelled
}
Вручную разорвать цикл:
Вызовите
cancel() вне
deinit, например, при закрытии экрана:
task?.cancel()
task = nil
Чего лучше избегать:
Явный захват метода
performSomeWork:
task = Task { [performSomeWork] in
repeat {
performSomeWork() // Метод всё равно содержит неявный self
} while !Task.isCancelled
}
Методы экземпляра неявно захватывают
self, даже если переданы как замыкание.
Как отлавливать такие проблемы:
- Тесты: Добавьте проверку на deinit с задержкой.
- Instruments: Используйте Leaks и Allocations для поиска утечки памяти и цикличных ссылок.
- Логирование: Добавьте print в deinit для критичных объектов.
Вывод:
- [weak self] - не панацея. Всегда анализируйте область видимости сильных ссылок.
- Для бесконечных задач (repeat, while) используйте локальный guard или явный cancel.
- Пишите тесты на освобождение памяти - это сэкономит часы отладки.