A memory leak on a flagship device is an inconvenience. The same leak on a 2GB or 3GB Android phone is a crash, usually thirty to forty minutes into a session, right after the player has made real progress. This is a field guide to finding leaks on the hardware where they actually matter, not the hardware where they are easiest to reproduce.
Android leaks are different from a simple out-of-memory crash. They build slowly, they rarely show up in a five-minute playtest, and the Unity Editor’s own memory behavior often hides them completely because the editor process has headroom a real device does not.
The editor’s memory allocator and the IL2CPP runtime on an actual Android device behave differently enough that a leak invisible in Play Mode can be obvious after fifteen minutes on a real phone. Any memory investigation that stops at the editor is not finished – it has barely started. Budget device time as part of the investigation, not as a final confirmation step.
Static references and event subscriptions. A UI panel that subscribes to a static event on Awake and never unsubscribes on Destroy keeps the whole object graph alive indefinitely. This is the single most common leak we find, and it is almost always invisible in a short session because the panel itself looks correctly destroyed – it is just not garbage collected.
The native-to-managed boundary. Native plugins, especially third-party ad and analytics SDKs, allocate memory that Unity’s managed heap view cannot see. A leak here shows up as rising total device memory with a flat or shrinking managed heap in the Memory Profiler, which is easy to misread as no leak if you only check one number.
Addressables and asset handles. Every AsyncOperationHandle that gets a Load call needs a matching Release call. Scenes that load and unload repeatedly without releasing handles will hold every previously loaded asset in memory, and the effect compounds the longer a session runs.
-> Take a Memory Profiler snapshot at app launch, before anything happens – this is your baseline.
-> Play through one full gameplay loop and take a second snapshot.
-> Diff the two snapshots and sort by retained size, not allocation count – a handful of large retained objects matters more than thousands of small ones.
-> For anything unexpected, check its reference tree back to a root – this almost alwayss the static reference or the un-released handle.
-> Repeat the loop three or four times and compare snapshot growth between loops. Linear growth per loop is the signature of a real leak, not noise.
Unsubscribe in the exact inverse order of subscription, and do it in OnDestroy, not OnDisable, unless the object is meant to be reusable. For anything that must reference a longer-lived object, consider whether it truly needs a strong reference or whether the relationship can be inverted with an event instead. For Addressables, wrap load and release in a single owning component’s lifecycle so there is exactly one place responsible for releasing what it loaded – shared ownership of a handle is where release calls get forgotten.
The engineers writing these articles are the ones who would work on your game.