The app doesn't crash immediately — it gradually grows in memory. After 20 minutes, slight lag appears. After 40, system Memory Pressure kills background processes. After an hour, SIGKILL from iOS or OOM-killer on Android terminates the app. The user thinks the app is glitchy. Firebase Crashlytics shows nothing — it's not a crash, it's a system kill. Even if Memory Warning triggers, the UI may become sluggish due to cache purges, leading to negative reviews. According to statistics, 80% of mobile apps have memory leaks, causing 15% monthly user loss. As mobile developers with 5 years of experience, we encounter such cases daily. Our engineers find leaks where standard tools stay silent. Savings on server costs can reach 30% after leak fixes. Contact us for a consultation on profiling your app.
How to Profile Memory in 5 Steps
- Tool setup: Connect Instruments for iOS, LeakCanary for Android, DevTools Memory for Flutter.
- Test scenario: Define key screens and actions (navigation, data loading, image handling).
- Data collection: Run profiler, execute scenario, capture heap snapshots before and after each action.
- Analysis: Look for objects that are not freed (retain cycles, listeners, context). Use LeakCanary for automatic detection.
- Optimization: Fix identified leaks (weak references, cancel subscriptions, close cursors). Re-run the test.
Detailed LeakCanary setup
Add via debugImplementation: `debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")`. After launch, if a leak occurs, a notification with the full stack trace appears.How to Find a Memory Leak on iOS?
Two templates for memory analysis in Instruments:
- Allocations — all memory allocations, live and dead objects. Generations (Generation button) let you compare objects before and after an action. If objects from a screen remain alive after closing it, there's a leak.
- Leaks — automatic detector of retain cycles. Red icon = cycle found. Shows a dependency graph with the culprits.
Classic retain cycle in Swift:
// Leak: ViewController holds closure, closure captures ViewController
class PhotoViewController: UIViewController {
var onPhotoLoaded: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onPhotoLoaded = {
self.imageView.image = UIImage(named: "photo") // strong capture
}
}
}
// Fixed:
onPhotoLoaded = { [weak self] in
self?.imageView.image = UIImage(named: "photo")
}
[weak self] is standard for any closures capturing self in long-lived objects. The Leaks tool will find this, but often shows the symptom, not the cause. Follow the graph to the stack, looking for the root strong reference.
According to Apple Instruments Documentation, using generations can detect up to 90% of leaks.
Why Do Images Eat Memory?
UIImage(named:) caches images in the system cache. Good for frequently used icons, bad for large photos loaded once. Use UIImage(contentsOfFile:) – no caching.
Image decoding happens on first display, not when creating UIImage. Pre-decode on a background thread:
func decodedImage(_ image: UIImage) -> UIImage {
UIGraphicsBeginImageContextWithOptions(image.size, true, 0)
defer { UIGraphicsEndImageContext() }
image.draw(in: CGRect(origin: .zero, size: image.size))
return UIGraphicsGetImageFromCurrentImageContext() ?? image
}
After this call, the image is decoded and stored as a bitmap in memory. Pass it to UI without decoding delay.
Android: Memory Profiler and LeakCanary
Android Studio Memory Profiler shows Heap in real time: Java Heap, Native Heap, Stack, Code, Graphics. The Dump Heap button saves a snapshot – analyze in hprof viewer or convert for Eclipse Memory Analyzer.
But the most useful tool in battle is LeakCanary. Add it in debugImplementation, it works automatically:
// build.gradle.kts
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
When a leak is detected, LeakCanary shows a notification with the full stack: what holds what, through which chain. No need to manually analyze heap dumps.
Common leak causes on Android:
- Context in static fields or singletons.
- Unclosed Cursor from ContentProvider or SQLiteDatabase.
- Listener not removed in onDestroy.
Comparison of profiling tools:
| Platform | Tool | Detection Method | Complexity |
|---|---|---|---|
| iOS | Instruments Allocations | Heap snapshots with generations | Medium |
| iOS | Instruments Leaks | Automatic retain-cycle search | Low |
| Android | Memory Profiler | Dump heap + MAT | High |
| Android | LeakCanary | Automatic monitoring | Low |
| Flutter | DevTools Memory | Object group snapshots | Medium |
LeakCanary finds leaks 10x faster than manual heap dump analysis. In practice, 80% of leaks are due to retain cycles or improper context management.
Typical leaks and their causes:
| Leak Type | Platform | Cause | Solution |
|---|---|---|---|
| Retain cycle in closure | iOS | Strong self capture | Use weak self |
| Context in static field | Android | Activity passed to singleton | Use Application context |
| Unclosed StreamSubscription | Flutter | Missing cancel in dispose | Call cancel in dispose |
What to Do with Cursor and Subscriptions?
override fun onStart() {
super.onStart()
locationManager.requestLocationUpdates(provider, 0, 0f, this)
}
override fun onStop() {
super.onStop()
locationManager.removeUpdates(this) // otherwise Activity won't die
}
Always close Cursor in a finally block. Remove location or sensor subscriptions in onStop()/onPause().
Flutter: Observatory and DevTools Memory
Flutter DevTools → Memory tab — snapshot profiler. Shows object groups by type. Dart:core, package:myapp — look at classes with unexpectedly high instance counts.
Typical Flutter leak — StreamSubscription without cancel():
class MyWidget extends StatefulWidget { ... }
class _MyWidgetState extends State<MyWidget> {
late StreamSubscription _sub;
@override
void initState() {
super.initState();
_sub = someStream.listen((event) { ... });
}
@override
void dispose() {
_sub.cancel(); // mandatory
super.dispose();
}
}
Without _sub.cancel() in dispose(), the subscription outlives the widget, holding a closure with a reference to State.
Our Approach
We perform full profiling using Apple Instruments Documentation and LeakCanary official site. Our engineers have 5+ years of experience. After the audit, you get a report with specific leaks and ready code fixes. The cost is calculated individually, but savings from leak elimination often pay for it within a month.
What's Included:
- Memory profiling via Instruments Allocations / Memory Profiler / DevTools
- LeakCanary setup for Android projects
- Heap-dump analysis and retain-cycle detection
- Image handling audit (caching, decoding)
- Pattern check for listeners, subscriptions, closures
- Report with specific leaks and fixes
Timeline: 2 to 3 days depending on app size. Request a consultation — we'll find leaks and suggest fixes. Contact us to get an estimate for your project.







