Memory issues can silently kill your app's performance and user experience. Fortunately, Xcode provides a powerful visual tool called the Memory Graph Debugger that helps you hunt down these issues without leaving your IDE.
What is the Memory Graph Debugger?
The Memory Graph Debugger is a visual debugging tool built into Xcode (available since Xcode 8) that captures a snapshot of all objects currently alive in your app's heap memory. It shows you:
- Every object instance in memory
- The relationships between objects
- Reference counts for each object
- Strong reference cycles (retain cycles)
- Memory leaks
Think of it as an X-ray for your app's memory - it freezes execution and lets you see exactly what's keeping each object alive.
Why Do You Need It?
Understanding Memory Management in iOS
iOS uses Automatic Reference Counting (ARC) to manage memory. ARC automatically tracks object references and deallocates objects when their reference count drops to zero. However, ARC can't break retain cycles - situations where two or more objects hold strong references to each other, preventing deallocation.
Common scenarios where memory issues occur:
- Retain Cycles: Two objects pointing to each other with strong references
-
Closure Capture: Closures capturing
selfstrongly - Delegate Patterns: Delegates held with strong references instead of weak
- Persistent Growth: Objects accumulating over time that should be released
Real-World Impact
- Memory leaks cause gradual performance degradation
- Excessive memory usage can lead to system termination of your app
- Retained observers can cause crashes and unpredictable behavior
- Poor memory management impacts battery life
How to Access the Memory Graph Debugger
Step 1: Enable Malloc Stack Logging (Recommended)
Before using the Memory Graph Debugger, enable malloc stack logging to see where objects were allocated:
- Click on your scheme in Xcode's toolbar (next to the Run/Stop buttons)
- Select "Edit Scheme..."
- Go to "Run" → "Diagnostics" tab
- Check "Malloc Stack" under Logging
- Select "Live Allocations Only" (to reduce overhead)
- Optionally check "Malloc Scribble" (fills freed memory to help detect use-after-free issues)
Step 2: Run Your App
Build and run your app (⌘R). Navigate through the features you want to test for memory issues.
Step 3: Capture the Memory Graph
There are two ways to access the Memory Graph Debugger:
Method 1: Debug Bar Button
- Look for the three-circle icon in the debug area (between the view debugger and location simulator)
- Click it to pause execution and capture the memory graph
Method 2: Debug Menu
- Go to Debug menu → Debug Memory Graph (or press ⌘⌥⇧M)
The app will pause, and Xcode will switch to the memory graph view.
Understanding the Memory Graph Interface
Once you capture a memory graph, you'll see three main areas:
1. Navigator Panel (Left)
Lists all objects organized by type with instance counts:
MyViewController (3)
0x123456789
0x123456790
0x123456791
DataProvider (5)
...
The number in parentheses shows how many instances exist in memory.
2. Visual Graph (Center)
Shows a node-based visualization of object relationships:
- Each box represents an object
- Arrows show references between objects
- Purple exclamation marks indicate detected leaks
- Self-referencing loops appear as arrows from an object back to itself
3. Inspector Panel (Right)
Displays detailed information:
- Object's class name
- Memory address
- Size in bytes
- Backtrace (if malloc stack logging is enabled)
- Reference count
- Class hierarchy
Key Features and Symbols
Memory Leak Indicators
Purple Exclamation Mark: Xcode has detected a potential memory leak. Note that Xcode's automatic detection doesn't catch everything - you still need to manually investigate.
Filtering Options
Use the filter bar at the bottom to focus your investigation:
- Show only leaked blocks: Filter to show only objects Xcode identified as leaked
- Show only content from workspace: Hide Apple framework objects to focus on your code
- Search field: Type class names to quickly find specific objects
Reference Types in the Graph
- Solid arrows: Strong references
- Dashed lines: Weak references
- Loop arrows: Self-references (object pointing to itself)
Practical Example: Finding and Fixing a Retain Cycle
Let's walk through a real example of finding and fixing a common retain cycle.
The Problem Code
class PhotoGalleryViewController: UIViewController {
var dataProvider: DataProvider?
override func viewDidLoad() {
super.viewDidLoad()
dataProvider = DataProvider()
dataProvider?.controller = self // Strong reference to self
}
}
class DataProvider {
var controller: PhotoGalleryViewController? // Strong reference back
func fetchData() {
// ... fetch data
controller?.updateUI() // Using controller
}
}
This creates a retain cycle: PhotoGalleryViewController → DataProvider → PhotoGalleryViewController
Finding It with Memory Graph
- Run your app and navigate to PhotoGalleryViewController
- Navigate away (pop the view controller)
- Repeat this 3-5 times
- Capture a memory graph
- Look for PhotoGalleryViewController in the navigator
What you'll see:
PhotoGalleryViewController (5)
You should see zero instances after navigating away, but instead you have 5! This indicates a memory leak.
- Click on one PhotoGalleryViewController instance
- In the visual graph, you'll see arrows forming a cycle:
┌─────────────────────────────┐
│ PhotoGalleryViewController │
│ (retained) │
└──────────┬──────────────────┘
│ dataProvider
↓
┌──────────────┐
│ DataProvider │
└──────┬───────┘
│ controller
└──────→ (cycles back)
The circular arrows clearly show the retain cycle.
The Fix
Change the strong reference to weak:
class DataProvider {
weak var controller: PhotoGalleryViewController? // Now weak!
func fetchData() {
controller?.updateUI()
}
}
Verify the Fix
- Run the app again
- Navigate to and from PhotoGalleryViewController several times
- Capture a memory graph
- Check PhotoGalleryViewController count
Result:
PhotoGalleryViewController (0) // Success!
Advanced Tips
Exporting Memory Graphs
You can save and share memory graphs:
- File → Export Memory Graph...
- Save the
.memgraphfile - Double-click to reopen in Xcode or share with team members
This is useful for:
- Analyzing issues later
- Collaborating with team members
- Comparing memory states before and after changes
Using with XCTest
Since Xcode 13, you can automatically capture memory graphs during UI tests:
func testMemoryUsage() throws {
let app = XCUIApplication()
measure(metrics: [XCTMemoryMetric()]) {
app.launch()
// Exercise your app
app.buttons["SaveMeal"].tap()
}
}
Run with enablePerformanceTestsDiagnostics flag to capture memory graphs on failure:
xcodebuild test \
-scheme YourApp \
-enablePerformanceTestsDiagnostics YES \
-resultBundlePath TestResults
Integration with Instruments
For deeper analysis, you can import memory graphs into Instruments:
- Capture a memory graph in Xcode
- Open Instruments
- Import the
.memgraphfile - Use the Allocations instrument for detailed timeline analysis
Command-Line Tools
For automation and CI/CD, use the leaks command:
# Check a memory graph file for leaks
leaks --fullContent YourApp.memgraph
Returns exit code 1 if leaks are detected.
Recent Updates (Xcode 16 / WWDC 2024)
The latest Xcode versions have enhanced the Memory Graph Debugger with:
- Better Integration with Instruments: Seamlessly import memory graphs into Instruments for timeline analysis
- Generation Marking: In Instruments' Allocations tool, mark generations to isolate memory growth during specific time periods
- Improved Detection: More accurate automatic leak detection
- Performance Metrics: Better integration with XCTest performance testing
Best Practices
- Run Through Critical Flows: Test your app's main user journeys multiple times before capturing
- Enable Malloc Stack Logging: Always enable it to see allocation backtraces
- Look for Patterns: If an object count matches your navigation count, you likely have a leak
- Focus on Your Code: Filter out framework objects initially
-
Check Delegates: Ensure delegate properties are marked
weak -
Test Closures: Look for
[weak self]or[unowned self]in closure capture lists - Regular Profiling: Make memory debugging part of your development cycle, not just when you have problems
Common Retain Cycle Patterns
Closure Captures
// Problem
class ViewController {
var completion: (() -> Void)?
func setup() {
completion = {
self.updateUI() // Strong capture
}
}
}
// Fix
completion = { [weak self] in
self?.updateUI()
}
Delegate Pattern
// Problem
class DataProvider {
var delegate: SomeDelegate? // Should be weak
}
// Fix
weak var delegate: SomeDelegate?
Timer/Observer Retention
// Problem: Timer retains target
Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(update),
userInfo: nil,
repeats: true)
// Fix: Invalidate in deinit or use weak reference patterns
Conclusion
The Memory Graph Debugger is an essential tool for iOS developers. By visualizing object relationships and making memory issues visible, it helps you:
- Catch retain cycles before they reach production
- Understand your app's memory behavior
- Debug issues that would otherwise be invisible
- Ship higher quality, more performant apps


Top comments (1)
memory graph shows you: