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 и течове на памет за секунди, без необходимост от анализ на суровите данни от heap dump.

Основни точки

  • 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 и итерация през регионите на алокация за изграждане на пълния граф. На ARM64 (Apple Silicon) допълнително се използват хардуерни средства за проследяване на алокации без забавяне. Времето за изграждане на графа зависи от размера на heap: за типично iOS приложение (50–200 MB) графът се изгражда за 1–3 секунди.

Според данни от Apple, Memory Graph е единственият инструмент, който може да визуализира retain cycles без модификация на кода или добавяне на инструментация. За разлика от 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. Кликвайки върху цикъла, виждате веригата от референции, образуващи затворения контур. На разработчика остава да определи кой от силните ръбове трябва да бъде слаб — обикновено това е референция от дъщерния обект към родителския (напр. delegate или closure).

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

    override func viewDidLoad() {
        super.viewDidLoad()
        // ❌ Цикъл retain: ViewController → услуга → 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 и процент от целия heap. Филтрирането по име на клас поддържа регулярни изрази.

Навигация в графа

Възлите на графа могат да се влачат за подобряване на четимостта. Двойното кликване върху възел отваря подробна информация за обекта: всички негови свойства с типове и стойности, стек на извикванията (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 намалява времето за диагностика на 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 може да причини срив при 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 след всяка голяма промяна в архитектурата — добавяне на нов делегат, closure или абонамент за NotificationCenter. Достатъчно е да изпълните типичен сценарий и да се уверите, че обектите се освобождават правилно и retain cycles липсват. Това отнема 2–3 минути, но предотвратява часове последващо дебъгване.

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

Memory Report в Xcode (раздел Debug Navigator) показва графика на консумацията на памет в реално време. Използвайте го заедно с Memory Graph: отваряйте Memory Graph при резко увеличение на консумацията. Например, при скролване на дълъг списък с клетки, зареждащи изображения, Memory Graph ще покаже кои обекти се създават и кои се освобождават. Ако броят на обектите расте без намаление — това е потенциален теч, видим преди да доведе до срив. Според данни от Apple, комбинацията Memory Graph + Memory Report е препоръчителният workflow за всички iOS разработчици, започвайки от Xcode 12.

objective-c
// Пример за теч в Objective-C чрез делегиране
@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;

Профилиране на closure-и

Обърнете специално внимание на closure-ите — най-честия източник на retain cycles в Swift. При улавяне на self вътре в closure, който се съхранява като свойство на обекта, се образува класически цикъл. Memory Graph показва това като closure (възел със символ {}), свързан със сини ръбове с уловените обекти. Редовно проверявайте всички closure-и, особено тези, използвани в асинхронни извиквания, GCD, Combine и SwiftUI. Според статистика на Point-Free, 90% от течовете в Swift проекти са свързани с closure-и, улавящи 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 с heap dump.

Обобщение

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

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също