Как решить проблему цикла сильных ссылок (retain cycle) в Swift?

Ответ

Retain cycle (сильная ссылочная цикличность) возникает, когда два или более объекта удерживают друг друга сильными (strong) ссылками, что предотвращает их освобождение ARC (Automatic Reference Counting). Это приводит к утечкам памяти.

Основные способы решения:

  1. weak ссылка: Используется, когда один объект не должен "владеть" другим, и ссылка может стать nil (например, делегат).

    class Parent {
        var child: Child?
    }
    class Child {
        weak var parent: Parent? // weak разрывает цикл
    }
  2. unowned ссылка: Используется, когда время жизни ссылаемого объекта гарантированно совпадает или превышает время жизни ссылающегося объекта, и ссылка никогда не должна быть nil. Обращение к unowned ссылке после освобождения объекта вызывает краш.

    class Customer {
        let id: String
        var card: CreditCard?
        init(id: String) { self.id = id }
    }
    class CreditCard {
        let number: String
        unowned let customer: Customer // У карты всегда есть владелец
        init(number: String, customer: Customer) {
            self.number = number
            self.customer = customer
        }
    }
  3. Захват списков в замыканиях: Замыкания — это ссылочные типы. Если замыкание, принадлежащее объекту, захватывает self сильно, возникает цикл.

    class NetworkManager {
        var completion: (() -> Void)?
    
        func fetchData() {
            makeRequest { [weak self] result in // Захватываем self слабо
                self?.handleResult(result) // self становится опциональным
            }
        }
    }
Ключевые различия: Ссылка Опциональность Поведение при освобождении объекта
strong Да/Нет Удерживает объект в памяти.
weak Всегда опциональна (T?) Автоматически становится nil.
unowned Всегда неопциональна (T) Вызовет краш при обращении.

Правило выбора: Используйте weak, если ссылка может стать nil в течение жизни объекта. Используйте unowned, только если вы абсолютно уверены, что ссылаемый объект не будет освобожден раньше.

Ответ 18+ 🔞

А, слушай, смотри, вот эта хрень с retain cycle — это же классика, блядь! Прямо как в анекдоте про двух мужиков, которые держат друг друга за жопу и кричат: «Ты меня отпусти — я тебя отпущу!» А нихуя не отпускают, вот и висят до скончания времён, ебать.

Суть в чём: два объекта хватают друг друга за горло сильными ссылками, и ARC такой: «Ну, раз вы друг другу так нужны — живите вместе вечно, нахуй». А память течёт, как вода из дырявого таза.

Как эту петлю разорвать, чтобы не было мучительно больно:

  1. weak — слабая ссылка. Это как сказать: «Я на тебя посмотрю, но если ты сдуешься — ну и хуй с тобой, я не расстроюсь». Используется, когда один объект не должен владеть другим в хардкорном режиме. Типичный пример — делегат. Делегат — он как зритель в театре, а не актёр на сцене.

    class Папаша {
        var детёныш: Дитя?
    }
    class Дитя {
        weak var батя: Папаша? // weak! Сын не владеет отцом. Если папа умрёт — сын просто скажет «ну нет и нет».
    }
  2. unowned — безусловная ссылка. Это похуистичнее, блядь. Ты говоришь: «Этот объект точно переживёт меня, я в этом уверен, как в том, что хуй — это член». Обращение к unowned ссылке после смерти объекта — это гарантированный краш, пиздец и вылет в далёкие ебеня. Используй, только если на 146% уверен.

    class Клиент {
        let id: String
        var карта: Кредитка?
        init(id: String) { self.id = id }
    }
    class Кредитка {
        let номер: String
        unowned let владелец: Клиент // Карта не существует без владельца. Это аксиома, ёпта.
        init(номер: String, владелец: Клиент) {
            self.номер = номер
            self.владелец = владелец
        }
    }
  3. Замыкания — вот где собака зарыта и где её чаще всего забывают! Замыкание — это тоже объект, сука. И если замыкание, которое живёт внутри объекта, хватает self сильно, получается та самая любовная петля на шее.

    class СетевойМенеджер {
        var completion: (() -> Void)?
    
        func получитьДанные() {
            сделатьЗапрос { [weak self] result in // ВАЖНО: захватываем self слабо, как последнюю бутылку пива!
                self?.обработатьРезультат(result) // И тут self — опциональный, надо через `?`
            }
        }
    }
Краткая шпаргалка, чтобы не ебал мозг: Ссылка Может быть nil? Что будет, если объект умер?
strong Да/Нет Держит мёртвеца в памяти. Призрак, блядь.
weak Да, всегда! (T?) Станет nil. Безопасно и некрашиво.
unowned Нет, никогда! (T) КРАШ, ёперный театр! Обращение к призраку как к живому.

Главное правило, чувак: Используй weak, если жизнь ссылки непредсказуема. А unowned — это как игра в русскую рулетку, только вместо револьвера — твоё приложение. Красиво, да?