Memory Graph è uno strumento visivo di Xcode Debug Navigator che mostra un grafo degli oggetti nella memoria dell'applicazione con i loro riferimenti reciproci. A differenza di un heap dump, Memory Graph mostra non solo un elenco di oggetti, ma un grafo orientato di riferimenti dove ogni nodo è un oggetto e ogni bordo è un riferimento (strong, weak, unowned). Secondo Apple WWDC 2018, lo strumento consente di rilevare visivamente retain cycles e perdite di memoria in pochi secondi, senza dover analizzare i dati grezzi dell'heap dump.
Punti chiave
Memory Graph è un componente di Xcode Debug Navigator (introdotto in Xcode 10, WWDC 2018) che costruisce un grafo orientato di tutti gli oggetti nella memoria del processo in debug. Ogni nodo del grafo è un'istanza di classe (Objective-C o Swift), ogni bordo è un riferimento a un altro oggetto. Il colore del bordo indica il tipo di riferimento: blu — strong, verde — weak, grigio — unowned. Il grafo è costruito sulla base dei dati di LLDB e dell'Objective-C runtime, quindi l'applicazione deve essere compilata in configurazione Debug con i simboli abilitati per un corretto funzionamento.
Come funziona: quando l'applicazione è in pausa su un breakpoint, Xcode richiede tramite LLDB al runtime tutti gli oggetti vivi e i loro riferimenti. LLDB utilizza objc_getClassList e itera attraverso le regioni di allocazione per costruire il grafo completo. Su ARM64 (Apple Silicon), vengono utilizzati ulteriori mezzi hardware per il tracciamento delle allocazioni senza rallentamenti. Il tempo di costruzione del grafo dipende dalla dimensione dell'heap: per un'applicazione iOS tipica (50–200 MB), il grafo viene costruito in 1–3 secondi.
Secondo Apple, Memory Graph è l'unico strumento in grado di visualizzare i retain cycles senza modifica del codice o aggiunta di strumentazione. A differenza di Instruments Leaks, Memory Graph funziona in tempo reale all'interno di Xcode e non richiede l'avvio separato del profiler. Questo lo rende il primo strumento di scelta per la diagnosi rapida delle perdite di memoria durante lo sviluppo.
Heap dump fornisce una tabella di tutti gli oggetti con numeri (shallow size, retained size) — ottimale per l'analisi quantitativa. Memory Graph fornisce un'immagine visiva delle connessioni — ottimale per trovare riferimenti ciclici. Gli strumenti si completano a vicenda: prima Memory Graph per il rilevamento rapido dei retain cycles, poi heap dump tramite Instruments Allocations per la misurazione precisa del retained size. Secondo objc.io, la combinazione di entrambi i metodi copre il 95% degli scenari di perdita di memoria.
Retain cycle è una situazione in cui due o più oggetti si tengono reciprocamente con riferimenti forti, formando un ciclo chiuso. ARC non può deallocare tale ciclo perché il retain count di ogni oggetto non raggiunge mai zero. Un esempio classico: ViewController e View, dove View ha un riferimento forte a una closure che cattura self (ViewController). Memory Graph mostra tali cicli come anelli (cicli), evidenziandoli per una rapida identificazione.
Quando Xcode rileva un retain cycle, lo evidenzia con un contorno arancione e mostra un avviso in Debug Navigator. Cliccando sul ciclo, si vede la catena di riferimenti che formano il ciclo chiuso. Lo sviluppatore deve solo determinare quale bordo strong dovrebbe essere weak — di solito è un riferimento da un oggetto figlio al genitore (ad esempio, delegate o closure).
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
}
}
In Memory Graph vedrai un triangolo: ViewController → DataService → closure → ViewController. La soluzione è rendere la cattura di self debole: [weak self]. Dopo la correzione, Memory Graph mostrerà un bordo verde dalla closure a ViewController e il retain cycle scomparirà.
// Codice corretto — cattura debole di self
service.fetchData { [weak self] data in
guard let self else { return }
self.updateUI(data)
}
L'interfaccia di Memory Graph Debugger è composta da tre pannelli: sinistro — un elenco di tutti gli oggetti vivi (raggruppati per classe) con il conteggio delle istanze; centrale — un grafo visivo con nodi trascinabili; destro — un ispettore per l'oggetto o il bordo selezionato. L'elenco degli oggetti mostra: icona della classe, numero di istanze in memoria, retained size totale e percentuale dell'intero heap. Il filtraggio per nome della classe supporta le espressioni regolari.
I nodi del grafo possono essere trascinati per migliorare la leggibilità. Un doppio clic su un nodo apre informazioni dettagliate sull'oggetto: tutte le sue proprietà con tipi e valori, lo stack di chiamate (backtrace) per ogni proprietà e la cronologia retain/release. Backtrace è una funzionalità chiave: mostra quale esatta riga di codice ha stabilito il riferimento all'oggetto. Questo consente di trovare la fonte della perdita senza rivedere manualmente tutto il codice.
Per grafi complessi, Xcode fornisce un layout automatico tramite Layout → Hierarchical o Cluster. Il layout gerarchico posiziona gli oggetti radice in alto e i figli in basso, semplificando la ricerca di catene. Il raggruppamento in cluster raggruppa oggetti correlati, utile quando il grafo contiene diversi gruppi isolati. Secondo Apple, per la maggior parte delle applicazioni si consiglia il layout gerarchico — è intuitivo e richiede meno tempo per l'analisi visiva.
// Comandi LLDB utilizzati internamente da 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
Un approccio sistematico all'analisi di Memory Graph include diverse fasi. Fase 1: esegui l'applicazione, svolgi uno scenario che potrebbe causare una perdita (aprire/chiudere una schermata, effettuare una richiesta di rete). Fase 2: clicca sul pulsante Memory Graph in Debug Navigator — Xcode costruisce il grafo. Fase 3: verifica gli avvisi arancioni di retain cycles nel pannello sinistro. Fase 4: per oggetti sospetti, usa l'opzione Show only cycles — verranno mostrati solo i nodi coinvolti in riferimenti ciclici.
Una volta trovato un retain cycle, clicca sul bordo del ciclo e apri il pannello dell'ispettore. La sezione Backtrace mostra lo stack di chiamate nel momento in cui questo riferimento è stato stabilito. Ad esempio, se il bordo porta da una closure a self, il backtrace mostrerà in quale metodo e in quale riga di codice la closure è stata creata. Questo elimina la necessità di indovinare — vedi immediatamente il punto in cui è stato creato il riferimento problematico. Secondo WWDC Labs, l'analisi del backtrace riduce il tempo di diagnosi del retain cycle da 15–20 minuti a 2–3 minuti.
class ProfileViewController: UIViewController {
var profileView: ProfileView!
override func viewDidLoad() {
super.viewDidLoad()
profileView = ProfileView()
// Memory Graph mostrerà il retain cycle qui
profileView.onTap = { [unowned self] in
// ⚠️ unowned può causare crash quando self è nil
self.navigateToDetail()
}
}
func navigateToDetail() { }
}
// ✅ Corretto: [weak self] + guard let self
profileView.onTap = { [weak self] in
guard let self else { return }
self.navigateToDetail()
}
Memory Graph può mostrare migliaia di oggetti, rendendo difficile la ricerca. Usa i filtri nel pannello sinistro: inserisci un nome di classe (ad esempio, ProfileViewController) per mostrare solo le istanze di quella classe. Quindi seleziona un'istanza che avrebbe dovuto essere deallocata (se la schermata è chiusa ma l'oggetto rimane). Applica Show Reachable From — verranno mostrati solo i riferimenti pertinenti a questo oggetto, nascondendo il resto del grafo.
Gli sviluppatori esperti usano Memory Graph non solo per trovare perdite, ma anche per il controllo proattivo della memoria. Controlla Memory Graph dopo ogni grande cambiamento architetturale — aggiunta di un nuovo delegate, closure o abbonamento a NotificationCenter. Basta eseguire uno scenario tipico e assicurarsi che gli oggetti vengano deallocati correttamente e che non ci siano retain cycles. Questo richiede 2–3 minuti, ma previene ore di debug successivo.
Memory Report in Xcode (scheda Debug Navigator) mostra un grafico del consumo di memoria in tempo reale. Usalo insieme a Memory Graph: apri Memory Graph quando il consumo aumenta improvvisamente. Ad esempio, scorrendo una lunga lista con celle che caricano immagini, Memory Graph mostrerà quali oggetti vengono creati e quali vengono deallocati. Se il numero di oggetti cresce senza diminuire — è una potenziale perdita visibile prima che causi un crash. Secondo Apple, la combinazione Memory Graph + Memory Report è il flusso di lavoro consigliato per tutti gli sviluppatori iOS a partire da Xcode 12.
// Esempio di perdita in Objective-C tramite delegation
@interface DownloadManager : NSObject
@property (strong) id delegate; // ❌ Deve essere weak!
@end
@implementation DownloadManager
// Memory Graph mostrerà il retain cycle:
// ViewController → DownloadManager.delegate → ViewController
@end
// Correzione: weak property
@property (weak) id delegate;
Presta particolare attenzione alle closure — la fonte più comune di retain cycles in Swift. Quando si cattura self all'interno di una closure memorizzata come proprietà di un oggetto, si forma un ciclo classico. Memory Graph lo mostra come una closure (un nodo con il simbolo {}) collegata da bordi blu agli oggetti catturati. Controlla regolarmente tutte le closure, specialmente quelle utilizzate in chiamate asincrone, GCD, Combine e SwiftUI. Secondo le statistiche di Point-Free, il 90% delle perdite nei progetti Swift è correlato a closure che catturano self.
Domande frequenti
Memory Graph funziona per entrambi i linguaggi perché utilizza l'Objective-C runtime. Gli oggetti Swift compatibili con ObjC (sottoclassi di NSObject marcate con @objc) vengono visualizzati completamente. Le strutture e le classi Swift pure senza ponte ObjC sono visibili con limitazioni.
Gli oggetti devono essere registrati nell'Objective-C runtime. I tipi valore Swift (struct, enum) non vengono visualizzati. Assicurati che la classe erediti da NSObject o utilizzi l'attributo @objc per la visibilità in Memory Graph.
Blu — riferimento strong, trattiene l'oggetto. Verde — riferimento weak, non influisce sul ciclo di vita. Grigio — riferimento unowned. Un retain cycle si forma solo da bordi blu.
La costruzione del grafo mette in pausa l'applicazione per 1–3 secondi e può aumentare temporaneamente il consumo di memoria di Xcode di 200–500 MB. L'applicazione stessa non viene rallentata poiché l'ispezione avviene durante la pausa del breakpoint.
Xcode non supporta l'esportazione diretta del grafo. Usa uno screenshot per la documentazione o lo script lldb heap.find_variable per l'estrazione programmatica dei dati. Per un'analisi dettagliata, usa Instruments Allocations con un heap dump.
Riepilogo
{}.Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche