Is that a memory leak, or just a cache?
4 min readMemoryPerformance
Memory graphs get misread more than any other counter. A line going up is not automatically a leak, and a leak is not automatically the reason the app died. Both mistakes send people down long, wrong investigations.
Shapes, not values
The absolute number matters much less than the shape over time.
Sawtooth. Climbs, drops, climbs, drops — and each drop returns to roughly the same floor. That is a cache doing its job, or an autorelease pool draining. Healthy.
Staircase. Climbs, plateaus, climbs, plateaus, never comes back down. Every step corresponds to something you did — a screen opened, a list loaded — and nothing is released when you go back. That is a leak, or at least an unbounded retention.
Ramp. A steady climb with no steps. Usually a repeating allocation, often a timer, an observer or a growing array nobody trims.
Cliff. Rises fast, then the process disappears. See below.
The test for a leak
Do the round trip, several times.
- Note the memory value on the main screen.
- Push into a detail screen, do the thing, come back.
- Note it again.
- Repeat five times.
If the value after each return is roughly the floor from step one, you are fine. If it is higher every time, something from that screen is still alive. Five iterations is enough to tell a leak apart from noise, and it is the cheapest diagnostic in mobile.
Why iOS makes this urgent
iOS does not swap. There is no paging file to absorb your mistake. Instead, when memory runs short, jetsam terminates processes — background ones first, and the foreground app if it is the one that grew.
The memory allowance is per-device and much lower than the installed RAM. A phone with 3 GB gives a foreground app far less than 3 GB, and considerably less than an 8 GB flagship gives. Which is why memory bugs so often present as “only crashes on the cheap phone”.
What a jetsam kill looks like from the outside
This is the part that costs teams the most time. A memory termination:
- Produces an entry in the crash list
- Has a backtrace, which is where the process happened to be, not where anything went wrong
- Has no exception, or one that makes no sense in context
- Is completely unreproducible on a newer device
People read the backtrace, find an innocent function at the top, and go and rewrite it. The backtrace is not the bug. The bug is the trend in the preceding two minutes.
The tell is always the same: check the memory trace ending at the moment of the kill. A staircase into a cliff is a jetsam. A flat line into a cliff is a real crash.
Common causes, in the order they turn up
Images decoded at full size
A 4000-pixel photo held 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 with no ceiling
NSCache evicts under pressure; a plain dictionary does not. A dictionary used as a cache is an unbounded allocation with a friendly name.
Retain cycles through closures
The classic. A closure captures self, self holds the closure, neither is ever released. Every screen that does this leaks its entire view hierarchy.
Observers and delegates that outlive their view
Registered on appear, 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, a “recent items” array with no trim. Fine for an hour, fatal for a session that lasts a day.
A workflow that finds these
- Capture during ordinary use, not a synthetic loop. Real navigation, real content.
- Watch the floor, not the peak. The value after returning to the root screen is the number that matters.
- Isolate by screen. Do the round trip on each major screen in turn. The one that raises the floor is where to look.
- Test on the smallest device you support. A leak that never gets killed on a flagship gets killed on the budget model, and that is where your users are.
- Re-run after the fix. Same trip, same count, compare the floor.
When the trend is not enough
Trend analysis tells you which screen leaks. It does not tell you which object. For that you eventually want a heap tool and a Mac. But most of the value is in narrowing “the app uses too much memory” down to “this screen adds 40 MB every visit” — after which the retain cycle is usually visible by reading the file.
