A memory leak happens when an object stays alive after the app no longer needs it. A screen that closes should release its views and listeners and background tasks. A leak keeps one of those objects reachable from a long lived owner. The garbage collector cannot reclaim it. Repeated use grows the heap little by little until the app slows or crashes. This guide explains how leaks form and how to find them and how to fix them for good.
Leaks differ from normal memory use. Normal use rises during a task and falls after the task ends. A leak rises during repeated use and never fully falls. Open a screen ten times and return each time. A healthy heap returns near its starting size. A leaking heap climbs with each round. That simple test separates a leak from a one time allocation spike. Run it early whenever users report growing slowness during long sessions.
Long lived owners cause most leaks. A static field and a singleton and an application object all live for the whole app life. Any reference from such an owner to a screen keeps that screen alive. A listener registered on a system service and never removed behaves the same way. A background task that holds a screen reference after the screen closes also blocks cleanup. Fixing a leak usually means breaking one reference chain at the right place.
class Worker : Runnable {
private val ref = WeakReference(screen)
override fun run() {
val s = ref.get() ?: return
s.onWorkDone()
}
}
Common leak patterns
Static references to screens are the classic leak. A static field that stores the current screen for easy access keeps every view of that screen alive. Replace the static screen reference with a weak reference only when a weak reference truly fits. Better still remove the static field and pass data through proper channels such as a view model or an event flow. A static field should hold data that has no link to a screen or a view.
Unregistered listeners leak quietly. A location listener and a sensor listener and a broadcast receiver all keep their owner reachable while registered. Register in a lifecycle method and unregister in the matching teardown method. Pair on start with on stop and pair on create with on destroy. Check the pairing for every listener you add. A listener added in two places but removed in one place leaks on every second use.
Inner classes can leak through an implicit reference to the outer screen. A non static inner class holds its outer object even when the task outlives the screen. A background thread or a delayed handler message with such a class keeps the screen alive until the task ends. Use a static nested class with a weak reference to the screen when a background task must touch the interface. Cancel pending tasks and messages when the screen closes so no stale callback fires later.
Detect leaks with tools
The memory profiler gives the first evidence of a leak. Record a heap dump after repeated use of a suspect screen. Look for many instances of the same screen class alive at once. One live instance may be the current screen. Several live instances point to a leak. Follow the retaining path from an old instance to its owner. The path names the field or listener that holds it. That name tells you where to fix.
Allocation recording shows which code path creates the objects that never die. Record allocations during the suspect flow and then force garbage collection. Objects that survive collection after the screen closed deserve attention. Group survivors by class and by allocation site. A single allocation site behind many survivors is a strong lead. Fix that site first and retest before chasing smaller groups.
A dedicated leak detection library can automate the same check during development. It watches screens as they close and reports retained instances after a delay. Use it in debug builds only. It adds overhead that does not belong in a release build. Treat its reports as leads and confirm each report with a heap dump. Automated detection finds candidates fast but human review confirms the real chain.
Fix the retaining chain
Remove the strong reference at the owner that should not hold the screen. Clear a static field when the screen closes if the field must exist. Unregister the listener in the matching lifecycle method. Cancel the background task or drop its screen reference on close. Each fix should target the exact link shown in the retaining path. Fixing a different link may hide the symptom for one test and leave the leak alive.
Caches need size limits and eviction rules. An unbounded image cache grows with every new image and never releases old entries. Set a maximum size based on available memory and evict the least recently used entries first. Clear the cache when memory pressure rises. A cache that respects limits is a feature. A cache without limits is a slow leak that looks like normal use until the heap fills.
View models and lifecycle aware components reduce leak risk by design. A view model survives rotation but does not hold a view reference. Data flows from the view model to the screen through observed state. When the screen closes the observation ends and the view stays collectable. Readers who want the wider performance view can pair this guide with the piece on testing app performance with profiler tools and confirm the heap returns to baseline after each fix.
Storage and media leaks
Media code leaks through bitmaps and cursors and open streams. A bitmap held by a static cache after its screen closes wastes memory for the rest of the session. A database cursor left open holds its result set and its connection. Close cursors and streams in a finally block or with automatic resource handling. Release large bitmaps when the screen that used them closes. Small resource leaks add up in apps that show many images.
Scoped storage code can also hold stale references when a media query keeps a cursor alive across screens. Close the query result as soon as the data is copied into a plain model. Keep only the model in memory and release the cursor at once. Readers who manage media files can continue with the piece on scoped storage and MediaStore APIs and apply the same close early rule to every media query they write.
Background work deserves a leak review too. A worker that retries forever with a screen reference never lets that screen die. Pass only the data the worker needs and never the screen itself. Use application context for long lived work instead of screen context. Screen context carries the whole view hierarchy with it. Application context carries far less and suits background tasks that outlive any single screen.
Rotation and state leaks
Rotation creates a new screen instance while the old instance should die. A leak during rotation shows up fast because each rotation adds another retained instance. Test rotation repeatedly with a heap dump after ten rotations. A healthy app keeps one current instance. A leaking app keeps many. Rotation leaks often come from a listener or a task that was not cancelled in the old instance teardown.
State saving can prevent a reload without causing a leak when done correctly. A state holder keeps data but not views. The new screen instance reads the data and rebuilds its views. Readers who fix rotation reloads can continue with the piece on handling orientation changes without a reload and keep the data holder free of view references. Data survives rotation. Views must not.
Test leak fixes with the same repeated use script that revealed the leak. Run the flow ten times and dump the heap. Confirm that old screen instances are gone after garbage collection. Run the flow on a low memory device as well because low memory exposes leaks sooner. A fix that passes on a flagship but fails on a low memory device still needs work.
Keep leaks out of future code
Add a leak check to the review checklist for every new screen. Ask which listeners the screen registers and where each one unregisters. Ask which background tasks the screen starts and how each one ends. Ask which static fields the feature touches. Three questions catch most leaks before they reach users. Make the checklist short so reviewers actually use it.
Add a repeated use test to the manual test plan for screens with lists and images and background work. Ten rounds of open and return takes a minute and reveals growth that a single pass hides. Record heap size before and after the rounds in the test report. A report with numbers makes a leak visible to the whole team. A report that says the screen felt fine hides the growth until users suffer it.
Leaks reward patience and precise fixes. Find the retaining path and break the right link and verify with a repeat test. Avoid broad fixes such as clearing every cache on every screen close because they hurt performance without fixing the chain. A precise fix keeps the app fast and keeps memory healthy across long sessions.
Reviewing leaks in code review
Code review catches leaks when reviewers ask the right questions. Ask which listeners a new screen registers. Ask where each listener unregisters. Ask which background tasks the screen starts and how they end. Ask whether any static field touches the screen. Four questions cover most leak sources. Add those questions to the review template so every reviewer asks them consistently.
Leak reports from users deserve a structured response. Ask for the device model and the app version and the steps that grow memory. Ask how long the session ran before slowness appeared. Reproduce with the same steps and the same device class when possible. A leak report without steps is hard to reproduce. A report with a repeatable script becomes a test case for the fix.
Long running apps such as messaging and music apps face leak risk most acutely. Users keep them open for hours or days. A small leak per screen open becomes a large heap after a day of use. Test long sessions with automated open and return loops. A loop that runs for an hour reveals growth that a five minute manual test hides. Automate the loop and record heap size at intervals.
Teach leak awareness to new team members with a worked example. Show a heap dump with several retained screens and walk through the retaining path. Show the one line fix that breaks the chain. A concrete example teaches more than a general warning about memory. New developers who see one real leak recognise the next one faster.


