iosNudge

How do you find and fix memory leaks in an iOS app?

MediumAsked oftenMemory & ARCToolingPerformance 3 min read

TL;DR

  1. Add a deinit log and check it runs when you leave a screen.
  2. Use Xcode's Debug Memory Graph to see which objects are alive and who holds them.
  3. Use Instruments (Leaks and Allocations) to find leaks and memory growth.
  4. 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:

  1. Mark a generation.
  2. Do an action (open and close a screen).
  3. 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

CauseFix
Stored closure captures self[weak self]
Strong delegateweak 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 observerRemove the token, [weak self]
Unbounded cacheSet a limit, use NSCache, clear on memory warning

Other useful tools

  • Xcode memory gauge (Debug navigator): quick view of memory over time.
  • leaks command line tool: run leaks <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 didReceiveMemoryWarning or UIApplication.didReceiveMemoryWarningNotification. Clear caches there.