Optimising app performance with LeakCanary
Memory leaks can make an Android app feel slow long before it crashes. A screen may disappear from view while its Activity, Fragment, adapter, bitmap, or database cursor remains in memory. After repeated navigation, the heap grows, garbage collection runs more often, and users experience dropped frames, delayed taps, or an eventual OutOfMemoryError.
LeakCanary gives Android developers a practical way to detect these retained objects during development. It watches destroyed components, creates a heap dump when something stays reachable too long, and presents a trace that points towards the retaining reference. Used with sensible lifecycle design and repeatable testing, it turns vague performance complaints into actionable fixes.
Why memory leaks affect Android performance
Android applications have a limited heap, especially on affordable phones that are common in the Australian market. An app used during a busy commute through Sydney or Melbourne may be opened and closed many times as users switch between maps, messages, and payment services. A small leak per screen visit can become significant during that pattern.
Leaked objects also increase garbage collection pressure. The device may pause application work to reclaim memory, causing animation stutter in a scrolling ListView, delayed dialog rendering, or a noticeably sluggish navigation drawer. Users on regional connections may already wait for network data, so extra local delays can make the entire product feel unreliable.
Common sources include non-static inner classes, listeners that are never removed, coroutines tied to the wrong scope, long-lived singletons holding an Activity, and callbacks from repositories into destroyed views. A retained Context can also keep an entire view hierarchy alive, including images and child controls that are no longer visible.
How LeakCanary finds retained objects
LeakCanary integrates with an Android app and observes lifecycle events such as an Activity being destroyed. When the watched object remains strongly reachable after a short delay, the library records a suspicion and can capture a heap dump. Its analysis identifies the path from a garbage-collection root to the object that should have been released.
Add the dependency to the debug configuration rather than the production build:
dependencies {
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
}
Recent versions generally initialise automatically through the Android startup mechanism. After installing a debug build, navigate away from a screen and wait for the notification or report. The result normally shows the leaked class, the number of retained instances, and a reference chain such as a singleton → listener → Activity.
A report is evidence, not a complete diagnosis. Check whether the object is genuinely unexpected, whether the leak repeats, and which application-owned reference keeps it alive. Framework references can appear in the trace even when the fix belongs in your Fragment, ViewModel, adapter, or repository.
Read heap traces before changing code
Start at the retained object and follow the chain upwards. If a destroyed DetailsActivity is held by a repository callback, inspect how that callback is registered and removed. If a Fragment view remains retained, look for binding fields that are cleared too late or observers attached to the view lifecycle rather than the Fragment lifecycle.
Lifecycle-aware components reduce many common errors. Collect Flow data with repeatOnLifecycle, use viewLifecycleOwner for view observers, and cancel work when a screen leaves the appropriate state. A ViewModel may safely outlive a configuration change, but it should not store a reference to an Activity or a view.
Dialogs and custom layouts deserve special care because they often capture screen objects through listeners. When building a reusable modal interface, the step-by-step guidance in custom dialog layouts can be paired with explicit dismissal and listener cleanup. Avoid placing a long-lived callback inside a dialog that keeps the original Activity alive after rotation.
Fix recurring leak patterns
A typical Fragment view-binding pattern is:
private var _binding: ScreenBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = ScreenBinding.bind(view)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
The binding belongs to the view lifecycle, not the Fragment object. Clear adapters, text watchers, touch listeners, and binding references in onDestroyView() when they point into that view tree. For a RecyclerView adapter, avoid keeping a Fragment or Activity unless the ownership is deliberate and short-lived.
Timers, threads, and observers need an explicit owner. Prefer lifecycleScope or viewLifecycleOwner.lifecycleScope over a manually managed global scope. Unregister broadcast receivers and sensor listeners, close cursors, and stop location updates when the related screen is no longer active. A database helper can be application-scoped, while a cursor should remain local to the operation that consumes it.
Static caches require the same discipline. Caching a drawable or parsed model may be useful, but caching a view, Activity, Fragment, or context created from a screen can retain a large object graph. Use applicationContext only when an operation genuinely needs application-wide context.
Build leak-resistant screens
Performance work is easier when each component has a clear owner. Define which class creates a listener, which class removes it, and which lifecycle event ends a request. This is particularly important in apps that combine SQLite data, SharedPreferences, animations, and custom dialogs, because each feature can introduce a different form of retained state.
Use this short routine while developing:
During development
- Rotate the device several times while a screen is open
- Enter and leave the screen repeatedly
- Trigger loading, errors, animations, and dialog dismissal
- Inspect whether retained instances continue increasing
Before release
- Test on an older and a current Android device
- Repeat navigation after process recreation
- Check long lists, image-heavy screens, and background work
- Review debug-only LeakCanary reports before shipping
Australian teams can reproduce realistic conditions by testing on common Samsung and Google phones alongside lower-cost Android models, rather than relying only on a high-end development handset. Verify behaviour across Telstra, Optus, and Vodafone connections as well; slow requests and retries can expose callbacks that remain active after a user leaves the screen.
Developer workflow matters as well. Keeping debugging notes and community references in shared development resources can help a distributed team record a leak trace, its cause, and the regression test that prevents it from returning.
Measure improvements and prevent regressions
LeakCanary should complement, rather than replace, Android Studio’s Memory Profiler. LeakCanary is excellent for finding unexpected retained objects automatically, while the profiler helps you compare heap size, allocation rates, and garbage collection during a complete user journey.
Record a baseline before making a fix. For example, open a list screen 20 times, rotate it five times, and note retained Activities, heap growth, and the time required for garbage collection. Apply the lifecycle correction, repeat the same journey, and confirm that the number of retained objects stabilises rather than merely dropping once.
| Approach | Best signal | Typical use | Limitation |
|---|---|---|---|
| LeakCanary | Retained object and reference path | Finding Activity, Fragment, and view leaks | Debug overhead and delayed reports |
| Android Studio Memory Profiler | Heap size and allocation trends | Comparing before-and-after behaviour | Requires manual investigation |
| Lifecycle inspection | Ownership and cancellation errors | Reviewing observers, jobs, and listeners | Does not prove an object is retained |
| Repeatable UI test | Regression across navigation flows | Protecting fixes in CI or QA | Needs carefully designed scenarios |
Keep LeakCanary enabled in debug and internal builds, where developers can act on reports without affecting customers. A clean report does not guarantee perfect memory usage, yet a recurring retained Activity or growing object count is a strong reason to investigate before release. This approach helps an Android app remain responsive across Australian networks, device ranges, and long daily sessions.