Memory leak — one of the most insidious problems in mobile development. The app’s memory usage steadily grows until it reaches the limit set by the OS, followed by an OutOfMemoryError or forced termination. According to Square Engineering, about 40% of Android apps have at least one memory leak that can only be detected through profiling. Let’s examine the causes and methods for preventing memory growth.
Key Takeaways
Memory leak — a situation where an object that is no longer needed by the app continues to be held in the heap because an active reference from the GC Root set still points to it. The garbage collector considers such an object alive and does not remove it.
Memory bloat — a broader problem where the app consumes more memory than necessary to perform its current tasks. Causes: excessive caching, object duplication, suboptimal data structures, and heap fragmentation.
In Android, each app is allocated a limited heap (typically 64–512 MB depending on the device and OS version). In iOS, the limit is less strict, but the system sends a memory warning when approaching the threshold.
| Characteristic | Android | iOS |
|---|---|---|
| Heap limit | 64–512 MB (depends on device) | Implicit (system) |
| Garbage collection | ART (Concurrent, Compact) | ARC (Automatic Reference Counting) |
| Leak mechanism | GC Root references | Retain cycles (strong reference cycles) |
| Outcome | OutOfMemoryError | Memory warning → termination |
According to the Facebook Engineering Blog, memory leaks cause ~15% of crash reports in mobile apps. On Android, this is compounded by ANRs due to frequent GC pauses when memory is low.
Static reference to Activity — a classic Android leak. If a static field or singleton holds a reference to an Activity, it will not be GC-collected even after finish() as long as the singleton is alive. An Activity is a heavy object containing a View hierarchy, resources, and Context.
object LeakHolder {
var activityRef: Activity ?= null // leak: static reference to Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
}
}
Anonymous classes and lambdas — implicitly hold a reference to the outer class. If a Runnable or Callback is passed to an external service and the Activity is destroyed, the anonymous class object still sits in the queue and prevents the Activity from being garbage collected.
In iOS, the main problem is retain cycles: two objects hold strong references to each other, and ARC cannot zero out the reference count for either. A typical case: a closure that captures self strongly, and self that holds a reference to the closure.
LeakCanary — a library from Square for automatic leak detection in Android. After an Activity or Fragment is destroyed, it checks whether the object was GC-collected. If not, it takes a heap dump and shows the leak trace.
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary auto-installs in debug build
// via ContentProvider — zero code setup
}
}
// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — a built-in tool for real-time memory monitoring. It allows you to record a heap dump, find suspicious objects (Retained Size > 1 MB), and trace the GC root path to each object.
For iOS, use the Xcode Memory Graph Debugger. It visualizes the object graph in memory, shows retain cycles, and lets you instantly detect circular references. Instruments > Allocations is also available for long-term monitoring.
WeakReference — a basic mechanism for references that should not interfere with garbage collection. If the GC decides to collect an object, WeakReference returns null. It is used for callbacks, listeners, and references to UI components from background threads.
Lifecycle-aware components — an architectural approach implemented in Android Jetpack (Lifecycle, LiveData, Flow, coroutines). Subscriptions are automatically cancelled on onDestroy, eliminating the main class of leaks.
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// coroutine auto-cancels on onCleared()
}
}
}
viewModelScope and lifecycleScope — built-in CoroutineScope in Android that are cancelled on the corresponding lifecycle event. This eliminates leaks through coroutines — the most common scenario in modern Android development.
Memory Profiler in Android Studio — the primary tool for heap monitoring. It shows live allocations, heap snapshots, and object counts by type. It allows you to record a dump and analyze it in MAT (Memory Analyzer Tool) to find suspicious objects.
Eclipse MAT — a desktop heap dump analyzer. After loading an HPROF file from Android Studio, MAT builds a dominator tree, shows the retain size of each object, and offers automatic leak suspect analysis through the Leak Suspects Report.
Xcode Memory Graph — a visual retain cycle debugger. When you click the Memory Graph Debugger button, Xcode stops the app, builds a complete object graph in memory, and highlights retain cycles in red.
| Tool | Platform | Feature |
|---|---|---|
| LeakCanary | Android | Auto-detection of leaks after destroy |
| Memory Profiler | Android Studio | Heap dump + live allocations |
| Eclipse MAT | Android | Dominator tree, Leak Suspects Report |
| Memory Graph | iOS (Xcode) | Retain cycles visualiser |
According to Google I/O 2023, apps using LeakCanary in debug builds reduce memory-related crashes by 30–50% in the first 2 months after adoption. It is recommended to add LeakCanary during the project onboarding stage.
Frequently Asked Questions
Leak — objects that are unreachable by code but not collected by GC due to active references. Bloat — the app holds objects that are logically needed but in excessive quantity (e.g., a 50 MB cache in an 80 MB running app). Bloat is fixed architecturally; leaks are fixed through correct reference management.
LeakCanary uses ObjectWatcher — after onDestroy() of an Activity, it creates a WeakReference to the Activity and triggers GC. If the WeakReference is not cleared after 5 seconds, LeakCanary takes a heap dump, analyzes the shortest reference chain from the GC Root to the object, and shows the exact leak stack with the file and line number.
Bitmap occupies memory outside the Java heap in native memory (native heap). The size of one Bitmap = width × height × 4 bytes (ARGB_8888). A 12 MP photo (4000×3000) takes 48 MB. Android cannot always free native memory promptly, so accumulating several Bitmaps leads to OOM even with sufficient Java heap.
Retain cycle — a situation in ARC where two objects hold strong references to each other, and the reference count never reaches zero. A typical example: a ViewController with a strong reference to a closure, and the closure captures self strongly. Solution: use [weak self] or [unowned self] in closures.
The heap size depends on the device and Android version. For older devices (API 15–24) — 64–128 MB. For modern ones (API 25+) — 256–512 MB. The exact value can be obtained via ActivityManager.getMemoryClass(). For large apps (games, editors), largeHeap=true in the manifest provides up to 1 GB.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also