How do you find and fix memory leaks in an iOS app?
TL;DR
- Add a
deinitlog and check it runs when you leave a screen. - Use Xcode's Debug Memory Graph to see which objects are alive and who holds them.
- Use Instruments (Leaks and Allocations) to find leaks and memory growth.
- Add unit tests that check an object is freed.
Most leaks in iOS apps are retain cycles from closures, delegates, timers or subscriptions.
Step 1: Does deinit run?
The fastest check.
final class CheckoutViewController: UIViewController {
deinit {
print("CheckoutViewController freed")
}
}Open the screen. Go back. If nothing prints, something still holds the controller.
Step 2: Memory Graph Debugger
While the app runs in Xcode, click the Debug Memory Graph button (three connected circles) in the debug bar.
- The left panel lists every live object by type.
- Leaked objects (unreachable cycles) get a purple "!" icon.
- Select an object to see a graph of everything that references it.
Tip: open and close a screen 3 times. If you see 3 copies of its view controller, it leaks.
Turn on Malloc Stack Logging (Scheme, Diagnostics, "Malloc Stack Logging: Live Allocations Only"). Then the graph also shows the stack trace where each object was created.
Step 3: Instruments
Product, Profile (Cmd+I), then pick a template.
Leaks
Finds memory that nothing can reach anymore, like retain cycles. It checks every few seconds and shows a red mark on the timeline when it finds a leak. Click it to see the cycle.
Allocations
Shows all memory over time. Use Mark Generation:
- Mark a generation.
- Do an action (open and close a screen).
- Mark again. Repeat a few times.
If each generation keeps growing, you have abandoned memory: objects that are still referenced but no longer needed. Leaks will not show these, but they still waste memory. A cache with no limit is a common example.
Step 4: Tests that catch leaks
With XCTest:
final class ProfileViewModelTests: XCTestCase {
func testViewModelIsFreed() {
var sut: ProfileViewModel? = ProfileViewModel(service: MockService())
weak var weakSUT = sut
sut?.load()
sut = nil
XCTAssertNil(weakSUT, "ProfileViewModel leaked")
}
}A reusable helper:
extension XCTestCase {
func trackForMemoryLeaks(_ object: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
addTeardownBlock { [weak object] in
XCTAssertNil(object, "Instance should be freed. Possible leak.", file: file, line: line)
}
}
}With Swift Testing:
import Testing
@Test func viewModelIsFreed() {
weak var weakSUT: ProfileViewModel?
do {
let sut = ProfileViewModel(service: MockService())
weakSUT = sut
sut.load()
}
#expect(weakSUT == nil)
}Common causes and fixes
| Cause | Fix |
|---|---|
Stored closure captures self | [weak self] |
| Strong delegate | weak var delegate with AnyObject protocol |
Repeating Timer | [weak self] + invalidate() |
Combine sink stored in self | [weak self] in the sink |
Never-ending Task (e.g. for await) | [weak self] + cancel the task |
Block-based NotificationCenter observer | Remove the token, [weak self] |
| Unbounded cache | Set a limit, use NSCache, clear on memory warning |
Other useful tools
- Xcode memory gauge (Debug navigator): quick view of memory over time.
leakscommand line tool: runleaks <process>on a Mac or simulator process, or in CI scripts.- Xcode Organizer and MetricKit: memory and termination data from real users.
What interviewers look for
- You do not guess. You measure with tools.
- You know the difference between a leak (unreachable cycle) and abandoned memory (reachable but unused).
- You add regression tests so the leak does not come back.
Common follow-up questions
- Why does the Leaks instrument miss some leaks? It only finds unreachable memory. Objects that are still referenced, for example by a singleton, are not "leaks" to it.
- Can the memory graph show false positives? Yes, sometimes for system objects or C buffers. Focus on your own types.
- Does a leak always crash the app? No. Memory grows until iOS terminates the app for using too much memory (a jetsam event).
- How do you check memory in production? MetricKit gives memory metrics and diagnostics. Xcode Organizer shows terminations caused by memory.
- What is a memory warning? iOS tells the app to free memory, via
didReceiveMemoryWarningorUIApplication.didReceiveMemoryWarningNotification. Clear caches there.