Skip to main content

Mobile App Memory Leaks: Finding Crashes and Retained Data

Find mobile app memory problems using repeatable navigation tests, heap analysis, allocation profiles and safe resource ownership on Android and iOS.

R
Written by Reiko

Ten gallery visits pass, but the eleventh ends in a crash. The images may be valid and the navigation code may appear unchanged. The failure can result from objects that remain reachable after their screens disappear, a growing cache or a short-lived allocation spike. Each requires a different investigation.

Classify the failure before calling it a leak

A memory leak retains resources beyond their intended lifetime. Excessive allocation can exhaust available memory even when everything would eventually be released. Memory corruption involves invalid access and can fail without a steady rise in usage. Operating-system termination under memory pressure is another outcome that should not automatically be counted as a conventional crash.

Collect the app build, device, system version, foreground state and available termination evidence. An Android out-of-memory exception, a native invalid access and an iOS memory-pressure termination lead to different diagnostic tools. An empty crash dashboard does not establish that no users were affected.

Pattern

Possible explanation

Next investigation

Usage rises after each identical navigation cycle

Retained screens, listeners or an unbounded cache

Compare surviving objects after cleanup

A large peak occurs while opening one image

Decoded pixels and temporary copies coexist

Inspect allocation size and image decoding

Failure follows a background-to-foreground transition

Process loss or invalid resource restoration

Examine lifecycle and termination records

Native access fails at inconsistent locations

Use-after-free or another corruption bug

Use suitable memory-access diagnostics

Measure comparable memory, not unrelated counters

The managed heap is only part of a mobile process. Native buffers, graphics resources, mapped files and framework allocations can also contribute. A stable Java heap does not prove that an Android game engine has stable native or graphics memory.

Record the tool and metric used. Resident Set Size and Proportional Set Size account for shared pages differently, so comparing one with the other can create a false regression. Compare like-for-like measurements at the same point in a repeatable route.

On Android, heap inspection and allocation profiling help locate retained managed objects, while native allocations need appropriate native-memory tools. In Xcode, memory graph inspection and Instruments provide complementary views of ownership and allocation behaviour. Check the tool's scope before assuming it sees every engine resource.

Build a navigation experiment with a stable endpoint

  1. Launch from a defined account and clear only test data that the experiment requires.

  2. Open a screen with representative images, media or game content.

  3. Return to the same lightweight starting screen.

  4. Wait for the documented cleanup or idle point.

  5. Repeat the route and compare retained objects and memory snapshots.

Suppose retained memory at that endpoint grows from 180 MiB to 260 MiB after 20 cycles. The net increase is 80 MiB, averaging 4 MiB per cycle. That suggests a repeatable retention problem, but it does not prove that a particular object leaks. A cache that intentionally fills to a fixed ceiling could initially show the same shape.

Continue long enough to distinguish a plateau from ongoing growth, and inspect the retained references. One lifetime path might run from an application-wide event dispatcher to a closed screen, which retains its image list. Fixing the dispatcher subscription can release the entire object graph; deleting individual images elsewhere may only hide the symptom.

Assign an owner to callbacks, tasks and observers

Every subscription needs a lifetime. A screen-scoped observer should not survive indefinitely because it was registered with a global service. Ensure cancellation or unregistration occurs through the relevant lifecycle path, including exceptional navigation and interrupted setup.

Asynchronous work can retain a screen while waiting for a slow response. Decide whether the task belongs to the screen, account or application. Cancelling the visible request does not necessarily cancel its underlying task, and a late callback can retain or update an obsolete interface.

On platforms using reference counting, strong-reference cycles can prevent otherwise unused objects from being released. Weak references can break an appropriate ownership cycle, but applying them indiscriminately can make required state disappear. Draw the intended ownership relationship before changing reference strength.

Control decoded images and temporary buffers

A 1,920 by 1,080 image decoded into four bytes per pixel requires 8,294,400 bytes, approximately 7.91 MiB, for that pixel buffer alone. Keeping 12 such buffers occupies about 94.92 MiB. This excludes metadata, additional copies and any other rendering resources.

Match decoding dimensions to the displayed size where the image pipeline supports it. A thumbnail grid usually does not need every photograph decoded at its original resolution. Put an explicit bound on memory caches and test eviction, not just cache hits.

Inspect transitions carefully. A fade between two screens may keep both image sets alive, while decoding the next screen adds temporary workspace. Average memory can remain acceptable even when that brief overlap causes failures. Measure the peak and identify which buffers coexist.

Release native resources through the right lifecycle

A garbage-collected wrapper does not guarantee prompt cleanup of a decoder, file descriptor or engine-managed texture. Follow the resource API's supported close or disposal mechanism. A finalizer is not a reliable schedule for releasing scarce resources.

Resource pools need bounds too. A pool that grows to accommodate every historical request is an unbounded cache under another name. Track checked-out resources, return paths and maximum retained capacity. Verify behaviour after cancellation and failed construction, where return logic is often skipped.

Separate allocation churn from retained growth

An animation may repeatedly allocate short-lived objects and trigger collection pauses without retaining them forever. A heap snapshot taken after cleanup can look healthy while scrolling still stutters. Record allocation activity during the interaction, not only the final amount of surviving data.

For a planning calculation, allocating 10,000 temporary objects of 128 bytes each per second creates 1,280,000 bytes of payload allocations per second. Over one minute that is 76.8 MB of cumulative allocation, not 76.8 MB necessarily resident at once. Object headers, alignment and collector behaviour change the real cost.

Reduce unnecessary work on frequent paths, then check both allocation rate and frame delivery. Reusing objects can help in a suitable design, but it introduces ownership and reset requirements. A pool that returns an object with another account's data is a correctness failure even if it reduces allocation.

Keep cleanup independent from account deletion

Releasing memory must not destroy recoverable player data. Persist necessary state before discarding a screen's transient objects. On account switching, clear account-scoped caches so the next profile cannot see a previous profile's private images or messages.

If a process dies, rebuilding the interface from durable state should be possible without reconstructing an entire old object graph. Separate a compact restore record from the in-memory representation used for rendering.

Verify the repair under the original conditions

Repeat the same route, sample points and number of cycles after the change. Confirm that the suspected objects disappear when their lifetime ends and that usage reaches a sensible steady state. Test a lower-memory device and the transition that previously produced the largest peak.

Use sanitizers for the classes of defects they detect, and use a release-like build for resource comparisons. Diagnostic instrumentation changes the workload. A repair that merely shifts memory from a measured heap into an unmeasured native allocation is not evidence of lower total usage.

What evidence belongs in the release decision?

Keep the reproduction route, before-and-after captures, identified ownership path and supported-device result. In production, compare failures by build and device group rather than assuming every memory-related termination has the same cause. The goal is predictable resource use with intact data and responsive navigation.

When a retained object appears in a memory graph, trace the reference path to its owner before deleting a cache. An image that remains reachable can be intentional while a screen is open. Repeat the same navigation route, leave the screen and compare after a stable waiting period. Record the number of retained instances and the corresponding native and graphics allocations to see whether the proposed fix addresses the reported crash.

Did this answer your question?