← Назад к вопросам

Что такое retain cycle и как его избежать?

1.6 Junior🔥 231 комментариев
#Управление памятью

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое retain cycle?

Retain cycle (цикл сильных ссылок) — это ситуация в управлении памятью в iOS/macOS разработке (при использовании ARC — Automatic Reference Counting), когда два или более объекта удерживают (retain) сильные ссылки друг на друга, создавая замкнутый цикл. В результате счётчик ссылок (reference count) этих объектов никогда не достигает нуля, они никогда не освобождаются из памяти, что приводит к утечке памяти (memory leak).

Механизм возникновения

Рассмотрим классический пример с двумя классами:

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) освобождён")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Апартаменты \(unit) освобождены")
    }
}

При создании циклической зависимости:

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

john?.apartment = unit4A
unit4A?.tenant = john

john = nil
unit4A = nil
// Ни один deinit не будет вызван!

После обнуления переменных john и unit4A счётчики ссылок остаются равными 1 (из-за взаимных сильных ссылок между объектами), поэтому объекты продолжают существовать в памяти, но становятся недоступными — это и есть retain cycle.

Способы избежания retain cycle

1. Использование weak ссылок

Weak (слабая) ссылка не увеличивает счётчик ссылок объекта. Когда объект, на который указывает weak-ссылка, освобождается, ссылка автоматически становится nil. Используется, когда один объект не должен "владеть" другим.

class Apartment {
    let unit: String
    weak var tenant: Person? // Слабая ссылка
    
    init(unit: String) {
        self.unit = unit
    }
}

2. Использование unowned ссылок

Unowned (бесхозная) ссылка также не увеличивает счётчик ссылок, но в отличие от weak, не становится nil при освобождении объекта. Используется, когда время жизни объектов совпадает или ссылаемый объект гарантированно существует дольше.

class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
}

class CreditCard {
    let number: String
    unowned let customer: Customer // Бесхозная ссылка
    
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

3. Использование capture lists в замыканиях

Замыкания (closures) захватывают ссылки на используемые объекты, создавая сильные ссылки по умолчанию. Это частая причина retain cycles.

Проблемный код:

class NetworkManager {
    var onUpdate: (() -> Void)?
    
    func start() {
        onUpdate = {
            self.doWork() // Захват сильной ссылки на self
        }
    }
    
    func doWork() { /* ... */ }
    
    deinit {
        print("NetworkManager освобождён")
    }
}

Решение через capture list:

func start() {
    onUpdate = { [weak self] in // Захват слабой ссылки
        self?.doWork()
    }
}

// Или для гарантированного существования:
func start() {
    onUpdate = { [unowned self] in
        self.doWork() // Рискованно, если self может быть nil
    }
}

4. Использование типов значений (value types)

Структуры (struct) и перечисления (enum) являются типами значений и не используют ссылочную семантику, поэтому не создают retain cycles по определению.

struct Point {
    var x, y: Int
    // Нет проблем с циклами, так как это value type
}

5. Паттерн делегирования (Delegation)

Стандартный подход в iOS — использование weak-ссылок для делегатов:

protocol MyDelegate: AnyObject { // Протокол только для классов
    func didUpdate()
}

class MyClass {
    weak var delegate: MyDelegate? // Всегда weak для делегата
}

Практические рекомендации

  • Всегда анализируйте отношения между объектами. Если связь "родитель-потомок", потомок обычно должен хранить weak/unowned ссылку на родителя.
  • Используйте инструменты диагностики: Instruments (Leaks, Allocations), Memory Graph Debugger в Xcode.
  • Будьте осторожны с замыканиями внутри классов — всегда используйте capture lists.
  • Для асинхронных операций используйте [weak self] и проверяйте существование через guard let self = self else { return }.
  • Избегайте глобальных синглтонов, которые захватывают ссылки на объекты.

Правильное управление ссылками критически важно для стабильности приложения, особенно на мобильных устройствах с ограниченными ресурсами памяти.