Memory Graph: что это, граф объектов и обнаружение циклических ссылок

Автор: IT Sectr Опубликовано: 2026-05-07 Время чтения: 10 мин

Memory Graph — визуальный инструмент Xcode Debug Navigator, отображающий граф объектов в оперативной памяти приложения с их взаимными ссылками. В отличие от heap dump, Memory Graph показывает не просто список объектов, а направленный граф ссылок, где каждый узел — объект, а каждое ребро — ссылка (strong, weak, unowned). По данным Apple WWDC 2018, инструмент позволяет визуально обнаруживать retain cycles и утечки памяти за секунды, без необходимости анализа сырых данных дампа кучи.

Главное

  • Memory Graph — визуальный граф объектов в памяти Xcode, показывающий ссылки между объектами в реальном времени.
  • Retain cycle обнаруживается по замкнутому контуру в графе — два или более объектов ссылаются друг на друга сильными ссылками.
  • Backtrace для каждого ребра графа показывает, где и когда была установлена ссылка, упрощая поиск источника утечки.
  • Фильтрация по имени класса и типу ссылки (strong/weak) позволяет быстро изолировать проблемные объекты.
  • Интеграция с Memory Report в Xcode позволяет отслеживать изменение потребления памяти в реальном времени.

Что такое Memory Graph и как он работает

Memory Graph — это компонент Xcode Debug Navigator (появился в Xcode 10, WWDC 2018), который строит направленный граф всех объектов в памяти отлаживаемого процесса. Каждый узел графа — экземпляр класса (Objective-C или Swift), каждое ребро — ссылка на другой объект. Цвет ребра указывает на тип ссылки: синие — strong, зелёные — weak, серые — unowned. Граф строится на основе данных LLDB и Objective-C runtime, поэтому для правильной работы приложение должно быть собрано в Debug-конфигурации с включёнными символами.

Принцип работы: когда приложение приостановлено на breakpoint, Xcode через LLDB запрашивает у runtime все живые объекты и их референсы. LLDB использует objc_getClassList и итерацию по allocation-регионам для построения полного графа. На ARM64 (Apple Silicon) дополнительно используются аппаратные средства для трекинга allocation без замедления. Время построения графа зависит от размера кучи: для типичного приложения iOS (50–200 MB) граф строится за 1–3 секунды.

По данным Apple, Memory Graph — единственный инструмент, который может визуализировать retain cycles без модификации кода или добавления instrumentation. В отличие от Instruments Leaks, Memory Graph работает в реальном времени внутри Xcode и не требует отдельного запуска профилировщика. Это делает его первым инструментом выбора для быстрой диагностики утечек памяти в процессе разработки.

Чем Memory Graph отличается от heap dump

Heap dump даёт таблицу всех объектов с числами (shallow size, retained size) — он оптимален для количественного анализа. Memory Graph даёт визуальную картину связей — оптимален для поиска циклических ссылок. Инструменты дополняют друг друга: сначала Memory Graph для быстрого обнаружения retain cycles, затем heap dump через Instruments Allocations для точного измерения retained size. По опыту objc.io, комбинация двух методов покрывает 95% сценариев утечек памяти.

Обнаружение retain cycles с помощью Memory Graph

Retain cycle — ситуация, когда два или более объектов удерживают друг друга сильными ссылками, образуя замкнутый контур. ARC не может освободить такой контур, поскольку retain count каждого объекта никогда не достигает нуля. Классический пример: ViewController и View, где View имеет strong reference на closure, который захватывает self (ViewController). Memory Graph отображает такие контуры в виде колец (циклов), подсвечивая их для быстрой идентификации.

Когда Xcode обнаруживает retain cycle, он выделяет его оранжевым контуром и показывает предупреждение в Debug Navigator. Щёлкнув по циклу, вы видите цепочку ссылок, образующих замкнутый контур. Разработчику остаётся определить, какое из strong-рёбер должно быть слабым — обычно это ссылка из дочернего объекта на родительский (например, delegate или closure).

swift
class ViewController: UIViewController {
    let service = DataService()

    override func viewDidLoad() {
        super.viewDidLoad()
        // ❌ Retain cycle: ViewController → service → closure → ViewController
        service.fetchData { self.updateUI($0) }
    }

    func updateUI(_ data: Data) {}
}

class DataService {
    var completion: ((Data) -> Void)?

    func fetchData(handler: @escaping (Data) -> Void) {
        self.completion = handler
    }
}

В Memory Graph вы увидите треугольник: ViewController → DataService → closure → ViewController. Решение — сделать захват self слабым: [weak self]. После исправления Memory Graph покажет зелёное ребро от closure к ViewController, и retain cycle исчезнет.

swift
// Исправленный код — слабый захват self
service.fetchData { [weak self] data in
    guard let self else { return }
    self.updateUI(data)
}

Интерфейс Memory Graph Debugger в Xcode

Интерфейс Memory Graph Debugger состоит из трёх панелей: левая — список всех живых объектов (сгруппированных по классам) с количеством экземпляров; центральная — визуальный граф с перетаскиваемыми узлами; правая — инспектор выделенного объекта или ребра. В списке объектов отображаются: иконка класса, количество экземпляров в памяти, общий retained size и процент от всей кучи. Фильтрация по имени класса поддерживает регулярные выражения.

Навигация по графу

Узлы графа можно перетаскивать для улучшения читаемости. Двойной клик по узлу открывает детальную информацию об объекте: все его свойства с типами и значениями, стек вызовов (backtrace) для каждого свойства и историю retain/release. Backtrace — ключевая функция: он показывает, какая именно строка кода установила ссылку на объект. Это позволяет найти источник утечки без ручного просматривания всего кода.

Для сложных графов Xcode предоставляет автоматическую раскладку через Layout → Hierarchical (иерархическая) или Cluster (кластерная). Иерархическая раскладка размещает корневые объекты сверху, дочерние — снизу, упрощая поиск цепочек. Кластерная группирует связанные объекты в кластеры, что удобно, когда граф содержит несколько изолированных групп. По данным Apple, для большинства приложений рекомендуется иерархическая раскладка — она интуитивно понятна и занимает меньше времени на визуальный анализ.

lldb
// LLDB-команды, используемые Memory Graph под капотом
(lldb) script import lldb.macosx.heap
(lldb) script heap.find_variable("viewController")
0x600000c4b80: ViewController
(lldb) script heap.refs 0x600000c4b80
0x600000c4b80 -> 0x600003a4c00 (DataService)
    ivar: _service, offset: 16

Анализ графа: поиск и устранение утечек

Систематический подход к анализу Memory Graph включает несколько этапов. Этап 1: запустите приложение, выполните сценарий, который потенциально вызывает утечку (открыть/закрыть экран, выполнить сетевой запрос). Этап 2: нажмите кнопку Memory Graph в Debug Navigator — Xcode построит граф. Этап 3: проверьте оранжевые предупреждения retain cycles в левой панели. Этап 4: для подозрительных объектов используйте опцию Show only cycles — отобразятся только узлы, входящие в циклические ссылки.

Использование backtrace для поиска источника

Когда retain cycle найден, щёлкните по ребру цикла и откройте панель инспектора. В разделе Backtrace показан стек вызовов в момент, когда эта ссылка была установлена. Например, если ребро ведёт от closure к self, backtrace покажет, в каком методе и на какой строке кода closure был создан. Это устраняет необходимость догадываться — вы сразу видите точку создания проблемной ссылки. По данным WWDC Labs, backtrace analysis сокращает время диагностики retain cycle с 15–20 минут до 2–3 минут.

swift
class ProfileViewController: UIViewController {
    var profileView: ProfileView!

    override func viewDidLoad() {
        super.viewDidLoad()
        profileView = ProfileView()
        // Memory Graph покажет retain cycle здесь
        profileView.onTap = { [unowned self] in
            // ⚠️ unowned может вызвать crash при nil self
            self.navigateToDetail()
        }
    }

    func navigateToDetail() { }
}

// ✅ Правильно: [weak self] + guard let self
profileView.onTap = { [weak self] in
    guard let self else { return }
    self.navigateToDetail()
}

Фильтрация лишних объектов

Memory Graph может показывать тысячи объектов, затрудняя поиск. Используйте фильтры в левой панели: введите имя класса (например, ProfileViewController), чтобы отобразить только экземпляры этого класса. Затем выберите экземпляр, который должен был быть освобождён (если экран закрыт, а объект остался). Примените Show Reachable From — отобразятся только ссылки, релевантные для этого объекта, скрывая остальной граф.

Практические советы по использованию Memory Graph

Опытные разработчики используют Memory Graph не только для поиска утечек, но и для проактивного контроля памяти. Проверяйте Memory Graph после каждого крупного изменения архитектуры — добавления нового делегата, замыкания или подписки на NotificationCenter. Достаточно выполнить типовой сценарий и убедиться, что объекты освобождаются корректно, а retain cycles отсутствуют. Это занимает 2–3 минуты, но предотвращает часы последующей отладки.

Комбинация с Memory Report

Memory Report в Xcode (вкладка Debug Navigator) показывает график потребления памяти в реальном времени. Используйте его вместе с Memory Graph: открывайте Memory Graph при резком росте потребления. Например, при скролле длинного списка с ячейками, загружающими изображения, Memory Graph покажет, какие объекты создаются, а какие — освобождаются. Если количество объектов растёт без снижения — это потенциальная утечка, видимая до того, как она приведёт к crash. По данным Apple, комбинация Memory Graph + Memory Report — recommended workflow для всех iOS-разработчиков, начиная с Xcode 12.

objective-c
// Пример утечки в Objective-C через delegation
@interface DownloadManager : NSObject
@property (strong) id delegate; // ❌ Должно быть weak!
@end

@implementation DownloadManager
// Memory Graph покажет retain cycle:
// ViewController → DownloadManager.delegate → ViewController
@end

// Исправление: weak property
@property (weak) id delegate;

Профилирование замыканий

Особое внимание уделяйте замыканиям (closures) — самый частый источник retain cycles в Swift. При захвате self внутри замыкания, которое хранится как свойство объекта, образуется классический цикл. Memory Graph отображает это как замыкание (узел с символом {}), соединённое синими ребрами с захваченными объектами. Регулярно проверяйте все closures, особенно используемые в асинхронных вызовах, GCD, Combine и SwiftUI. По статистике Point-Free, 90% утечек в Swift-проектах связаны с замыканиями, захватывающими self.

Часто задаваемые вопросы

Memory Graph работает только для Objective-C или для Swift тоже?

Memory Graph работает для обоих языков, так как использует Objective-C runtime. Swift-объекты, совместимые с ObjC (наследники NSObject, отмеченные @objc), отображаются полностью. Чистые Swift-структуры и классы без ObjC-мостирования видны ограниченно.

Почему Memory Graph не показывает некоторые объекты?

Объекты должны быть зарегистрированы в Objective-C runtime. Swift value types (struct, enum) не отображаются. Убедитесь, что класс наследует NSObject или использует @objc атрибут для видимости в Memory Graph.

Как интерпретировать цвета рёбер в графе?

Синий — strong reference, удерживает объект. Зелёный — weak reference, не влияет на жизненный цикл. Серый — unowned reference. Retain cycle образуется только из синих рёбер.

Замедляет ли Memory Graph приложение?

Построение графа приостанавливает приложение на 1–3 секунды и может временно увеличить потребление памяти Xcode на 200–500 MB. Само приложение не замедляется, так как инспекция происходит во время паузы на breakpoint.

Можно ли экспортировать Memory Graph для анализа?

Xcode не поддерживает экспорт графа напрямую. Используйте скриншот для документации или lldb скрипт heap.find_variable для программного извлечения данных. Для детального анализа используйте Instruments Allocations с дампом кучи.

Итоги

  • Memory Graph — визуальный инструмент Xcode для отображения графа объектов в памяти с их ссылками.
  • Retain cycle отображается как замкнутый контур из синих (strong) рёбер — Xcode подсвечивает его оранжевым.
  • Backtrace для каждого ребра графа показывает точное место в коде, где была создана проблемная ссылка.
  • Фильтрация по классам и типу ссылок позволяет изолировать утечки в графе с тысячами объектов.
  • Замыкания — основной источник retain cycles в Swift, Memory Graph отображает их как узлы {}.
  • Weak и unowned — решения для разрыва цикла, но weak предпочтительнее из-за safety при nil.
  • Регулярная проверка Memory Graph после изменений архитектуры предотвращает память regression в проекте.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также