Memory Consumption Testing for Mobile Applications
App doesn't crash immediately — it gradually grows in memory. After 20 minutes of use slight sluggishness appears. After 40 — system Memory Pressure kills background processes. After hour — SIGKILL from iOS or OOM-killer Android terminates app. User thinks app glitches. Firebase Crashlytics shows nothing — not a crash, it's kill by system.
iOS: Instruments Allocations and Leaks
Two templates for memory analysis in Instruments:
Allocations — all memory allocations, live and dead objects. Generations (Generation button) allow comparing objects before and after action. If action screen objects remain in live memory after screen closed — leak.
Leaks — automatic retain-cycle detector. Red icon = found cycle. Shows dependency graph with 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
}
}
}
// Correct:
onPhotoLoaded = { [weak self] in
self?.imageView.image = UIImage(named: "photo")
}
[weak self] — standard for any closure capturing self in long-lived objects. Instruments Leaks will find it, but often shows symptom, not cause. Follow graph path in stack, find root strong reference.
UIImage and Memory
UIImage(named:) caches image in system cache. Good for often-used icons. Bad for large photos loaded once. Use UIImage(contentsOfFile:) — doesn't cache.
Image decoding happens at first display, not at UIImage creation. Pre-decode on 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 image already decoded and lies in memory as bitmap. Pass to UI without decode delay.
Android: Memory Profiler and LeakCanary
Android Studio Memory Profiler shows Heap in real time: Java Heap, Native Heap, Stack, Code, Graphics. Dump Heap button saves snapshot — analyze in hprof viewer or convert for Eclipse Memory Analyzer.
But most useful tool in battle — LeakCanary. Plugs into debugImplementation, works automatically:
// build.gradle.kts
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
When leak found LeakCanary shows notification with full stack: what holds what, through what chain. No manual heap-dump analysis needed.
Common leak causes on Android:
Context in static fields or singletons:
// Bad
object ImageCache {
var context: Context? = null // holds Activity
}
// Right: applicationContext
object ImageCache {
lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext // not Activity
}
}
Unclosed Cursor from ContentProvider or SQLiteDatabase:
val cursor = db.query(...)
try {
// work with cursor
} finally {
cursor.close() // mandatory, even on exception
}
Listener not removed on onDestroy:
override fun onStart() {
super.onStart()
locationManager.requestLocationUpdates(provider, 0, 0f, this)
}
override fun onStop() {
super.onStop()
locationManager.removeUpdates(this) // otherwise Activity won't die
}
Flutter: Observatory and DevTools Memory
Flutter DevTools → Memory tab — snapshot profiler. Shows object groups by type. Dart:core, package:myapp — look at classes with unexpected large instance counts.
Typical leak in Flutter — 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() subscription lives longer than widget, holds closure with State reference.
What's Included
- Memory profiling via Instruments Allocations / Memory Profiler / DevTools
- LeakCanary setup for Android project
- Heap-dump analysis and retain-cycle search
- Image handling audit (cache, decoding)
- Pattern check for listeners, subscriptions, closures
- Report with specific leaks and fixes
Timeline
2–3 days depending on app size and found problems count. Cost is calculated individually.







