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

Ответ

Retain cycle возникает, когда два или более объекта удерживают друг друга через strong-ссылки, предотвращая их освобождение ARC. Методы обнаружения:

1. Инструменты Xcode:

  • Debug Memory Graph (кнопка с графом в нижней панели отладчика). Показывает визуальный граф объектов в памяти. Циклы отображаются как замкнутые связи. Позволяет сразу увидеть, какие объекты участвуют в цикле.
  • Instruments → Leaks. Запустите профилирование для обнаружения утечек памяти в runtime.

2. Профилактика в коде:

  • Всегда используйте [weak self] или [unowned self] в замыканиях, когда self может удерживать это замыкание.

    class DataLoader {
    var onComplete: (() -> Void)?
    
    func loadData(completion: @escaping () -> Void) {
        self.onComplete = completion
        // ... загрузка
    }
    
    func setupHandler() {
        loadData { [weak self] in // Разрываем потенциальный цикл
            self?.handleData()
        }
    }
    }
  • Для паттерна делегата всегда объявляйте свойство делегата как weak.
    protocol MyDelegate: AnyObject { ... }
    class MainClass {
    weak var delegate: MyDelegate?
    }

3. Дополнительная настройка:

  • Включение Zombie Objects (в схеме → Diagnostics) помогает отловить обращение к уже освобождённому (deallocated) объекту, что может быть косвенным признаком проблем с памятью.

Ответ 18+ 🔞

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

Во-первых, есть в Xcode штуки, которые в глаза тычут. Вот кнопка Debug Memory Graph — эта, с графом, внизу в отладчике. Жмёшь её, а она тебе рисует всю эту паутину, кто кого держит. И если видишь, что объекты по кругу ходят, как мудаки вокруг столба, — вот он, цикл, нарисовался. Прямо визуально, блядь. Ещё можно в Instruments → Leaks залезть. Запускаешь профилирование, и если память уплывает, а объекты не освобождаются — значит, где-то этот ебучий цикл сидит.

Во-вторых, чтобы не было таких сюрпризов, надо код писать с мозгом. Всегда, блядь, в замыканиях используй [weak self] или [unowned self], особенно если self это замыкание в себе держит. Иначе получится, что ты сам себя за хвост ловишь, как та собака.

class DataLoader {
    var onComplete: (() -> Void)?

    func loadData(completion: @escaping () -> Void) {
        self.onComplete = completion
        // ... загрузка
    }

    func setupHandler() {
        loadData { [weak self] in // Смотри, слабая ссылка — цикл рвётся!
            self?.handleData()
        }
    }
}

А если делаешь делегата — так это вообще святое. Свойство делегата должно быть weak, иначе он тебе как пиявка присосётся намертво.

protocol MyDelegate: AnyObject { ... }
class MainClass {
    weak var delegate: MyDelegate? // Вот так, правильно, без фанатизма.
}

Ну и на закуску, для параноиков. Можно Zombie Objects включить (в схеме → Diagnostics). Это когда объект уже помер, а к нему обращаются — он тебе сигнал подаст. Иногда так косвенно и выясняется, что памятью кто-то неправильно играл. Но это уже, как говорится, для тонких ценителей, когда совсем пиздец и ничего не понятно.