Is that a memory leak, or just a cache?
4 min readMemoryPerformance
Memory graphs are misread more often than any other counter. A rising line is not necessarily a leak, and a leak is not necessarily why the app terminated. Both mistakes lead to long investigations in the wrong direction.
Shapes, not values
The absolute number matters much less than the shape over time.
Sawtooth. Memory climbs and drops repeatedly, and each drop returns to roughly the same baseline. This is a cache working as intended, or an autorelease pool draining. Healthy.
Staircase. Memory climbs, levels off, climbs again, and never comes back down. Each step matches an action, such as opening a screen or loading a list, and nothing is released when you go back. This is a leak, or at least unbounded retention.
Ramp. A steady climb without steps. Usually a repeating allocation, often from a timer, an observer or a growing array that is never trimmed.
Cliff. Memory rises quickly, then the process disappears. See below.
How to test for a leak
Repeat a round trip several times.
- Note the memory value on the main screen.
- Open a detail screen, perform the action, and go back.
- Note the value again.
- Repeat five times.
If the value after each return is close to the baseline from step 1, there is no leak. If it is higher each time, something from that screen is still in memory. Five rounds are enough to separate a leak from noise, and this is the cheapest diagnostic in mobile development.
Why this is urgent on iOS
iOS does not swap memory to disk. Instead, when memory runs short, jetsam terminates processes: background processes first, then the foreground app if it is the one that grew.
The memory limit depends on the device and is much lower than the installed RAM. On a phone with 3 GB, a foreground app gets far less than 3 GB, and much less than it would get on an 8 GB flagship. This is why memory bugs so often appear as “only crashes on the cheaper phone”.
What a jetsam termination looks like from outside
This is the part that costs teams the most time. A memory termination:
- Creates an entry in the crash list
- Has a backtrace showing where the process happened to be, not where anything went wrong
- Has no exception, or one that makes no sense in context
- Cannot be reproduced on a newer device
Developers often read the backtrace, find an unrelated function at the top, and rewrite it. The backtrace is not the bug. The bug is the memory trend over the preceding two minutes.
The sign is consistent: check the memory trace up to the moment of termination. A staircase ending in a cliff is a jetsam termination. A flat line ending in a cliff is a real crash.
Common causes, in the order they usually appear
Images decoded at full size
A 4000-pixel photo kept in memory for a 200-pixel thumbnail. This is both a performance bug and a memory bug, and in list-heavy apps it is usually the first and largest finding.
Caches without a limit
NSCache evicts under pressure; a plain dictionary does not. A dictionary used as a cache is an unbounded allocation with a misleading name.
Retain cycles through closures
A classic case. A closure captures self, self holds the closure, and neither is ever released. Every screen that does this leaks its entire view hierarchy.
Observers and delegates that outlive their view
Registered when the view appears and never removed. The controller stays alive to receive notifications for a screen the user left ten minutes ago.
Growing arrays
A log buffer, a message list or a “recent items” array with no limit. Fine for an hour, fatal in a session that lasts a day.
A workflow that finds these
- Capture during normal use, not a synthetic loop. Use real navigation and real content.
- Watch the baseline, not the peak. The value after returning to the root screen is the one that matters.
- Test one screen at a time. Repeat the round trip on each major screen. The screen that raises the baseline is where to look.
- Test on the lowest-memory device you support. A leak that never triggers termination on a flagship will on a budget model, and that is where many of your users are.
- Re-run after the fix. Use the same round trip and count, and compare the baseline.
When the trend is not enough
Trend analysis tells you which screen leaks, not which object. For that, you eventually need a heap tool and a Mac. But most of the value comes from narrowing “the app uses too much memory” down to “this screen adds 40 MB every visit”. After that, the retain cycle is usually visible from reading the code.
