Как управлять жизненным циклом и избегать утечек памяти при работе с классами (ссылочными типами) в Swift?

«Как управлять жизненным циклом и избегать утечек памяти при работе с классами (ссылочными типами) в Swift?» — вопрос из категории Swift Core, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Ссылочные типы (class) в Swift управляются через Automatic Reference Counting (ARC). Память освобождается, когда счетчик ссылок на объект становится равным нулю.

Основные инструменты управления:

  • strong (сильная ссылка, по умолчанию): Увеличивает счетчик ссылок. Объект не будет уничтожен, пока существует хотя бы одна сильная ссылка на него.
  • weak (слабая ссылка): Не увеличивает счетчик ссырок. Автоматически становится nil, когда объект освобождается. Всегда должна быть опциональной (var).
    weak var delegate: MyDelegate?
  • unowned (бесхозная ссылка): Аналогична weak, но не является опциональной. Предполагает, что объект будет существовать дольше, чем ссылка на него. При обращении к освобожденному unowned объекту произойдет краш.
    unowned let parentViewController: UIViewController // Используется, когда parent гарантированно живет дольше

Проблема: Цикл сильных ссылок (Retain Cycle) Возникает, когда два объекта сильно ссылаются друг на друга, не позволяя ARC освободить память.

class Person {
    var apartment: Apartment? // Сильная ссылка на Apartment
    deinit { print("Person освобожден") }
}

class Apartment {
    var tenant: Person? // Сильная ссылка на Person
    deinit { print("Apartment освобожден") }
}

var john: Person? = Person()
var unit4A: Apartment? = Apartment()

john?.apartment = unit4A // Apartment.refCount = 2
unit4A?.tenant = john    // Person.refCount = 2
// ЦИКЛ СОЗДАН!

john = nil    // Person.refCount = 1 (осталась ссылка из unit4A.tenant)
unit4A = nil  // Apartment.refCount = 1 (осталась ссылка из john.apartment)
// Оба объекта НИКОГДА не будут освобождены, утечка памяти.

Решение: Разорвать цикл, заменив одну из сильных ссылок на weak или unowned.

class Apartment {
    weak var tenant: Person? // Теперь ссылка слабая
}
// После этого при обнулении john и unit4A оба объекта успешно деинициализируются.

Типичные сценарии:

  • Делегаты и протоколы: Ссылки на делегаты почти всегда должны быть weak.
  • Замыкания (Closures): Замыкания захватывают (capture) сильные ссылки на используемые объекты. Используйте список захвата [weak self] или [unowned self], чтобы избежать цикла, если замыкание хранится в самом захватываемом объекте.
    networkService.fetchData { [weak self] result in
    guard let self = self else { return } // Проверка на существование
    self.updateUI(with: result)
    }