Какие инструменты в Xcode и iOS используются для отладки утечек памяти и сильных ссылочных циклов (retain cycles)?

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

Ответ

Для обнаружения утечек памяти и retain cycles в iOS-разработке применяется комбинация инструментов Xcode и методик написания кода.

1. Debug Memory Graph (Визуализатор памяти)

Самый быстрый и наглядный инструмент, встроенный в Xcode.

Как использовать:

  1. Запустите приложение в режиме отладки.
  2. Выполните действия, которые могут вызывать утечку.
  3. Нажмите кнопку Debug Memory Graph (значок из трех кругов) на панели отладки.
  4. Xcode отобразит граф всех живых объектов в памяти. Пурпурные значки с восклицательным знаком указывают на потенциальные циклы сильных ссылок.
  5. Щелкните на объекте, чтобы увидеть цепочку владения (кто на кого ссылается).

Что искать:

  • Два объекта, которые ссылаются друг на друга через сильные (strong) ссылки.
  • Классы (например, ViewController), которые не освобождаются после закрытия экрана.

2. Instruments

Более мощный и детализированный инструмент для профилирования.

  • Allocations: Позволяет отслеживать выделение и освобождение памяти по типам объектов. Ищите постоянно растующее число определенных объектов.
  • Leaks: Специализированный инструмент для поиска утечек. Он показывает объекты, на которые больше нет ссылок из "корней" (unreachable), но память для которых не была освобождена.

Как использовать:

  1. В Xcode: ProductProfile (или Cmd+I).
  2. Выберите шаблон Leaks или Allocations.
  3. Запишите сессию, взаимодействуя с приложением.
  4. Анализируйте стек вызовов для найденных утечек.

3. Логирование жизненного цикла

Ручное добавление логов в deinit для отслеживания освобождения объектов.

class MyViewController: UIViewController {
    deinit {
        print("(type(of: self)) был освобожден")
    }
}

Если это сообщение не появляется при ожидаемом закрытии контроллера — есть утечка.

4. Типичные причины retain cycles и их решение

Пример 1: Замыкание (Closure), захватывающее self

class DataManager {
    var data: [String] = []
    var onDataUpdated: (() -> Void)? // Сильная ссылка на замыкание

    func loadData() {
        // ПРОБЛЕМА: Замыкание сильно захватывает `self`.
        fetchFromNetwork { [self] newData in
            self.data = newData
            self.onDataUpdated?() // Если onDataUpdated также ссылается на это замыкание? Цикл!
        }
    }
}

Решение: Использовать weak или unowned захват.

fetchFromNetwork { [weak self] newData in
    guard let self = self else { return }
    self.data = newData
    self.onDataUpdated?()
}

Пример 2: Делегат (Delegate)

class ChildView {
    var delegate: MyDelegate? // Сильная ссылка на делегата
}
class ParentController: MyDelegate {
    let childView = ChildView()

    init() {
        childView.delegate = self // Родитель сильно владеет childView, а childView сильно ссылается на родителя -> ЦИКЛ.
    }
}

Решение: Объявить делегата как weak.

protocol MyDelegate: AnyObject { ... } // Протокол должен быть class-bound

class ChildView {
    weak var delegate: MyDelegate?
}

Лучшие практики:

  • Всегда используйте weak для ссылок на делегаты и источники данных (data sources).
  • Для захвата self в замыканиях, которые могут пережить объект, используйте [weak self].
  • Используйте unowned только когда вы абсолютно уверены, что захваченный объект будет жить дольше замыкания (иначе — краш).
  • Для реактивного программирования (RxSwift, Combine) не забывайте о DisposeBag и AnyCancellable для управления подписками.