Detecting and Fixing Memory Leaks in Mobile Applications
Application kills iOS when user opens and closes map screen 20 times in a row. Crash log contains Terminated due to memory pressure. In Instruments → Allocations visible: each opening of MapViewController adds ~12 MB to heap and these 12 MB are never released. After 20 openings — 240 MB from map screen alone. This is memory leak — not "possibly", but definitely.
Memory Leak Mechanics: Why Objects Are Not Released
Retain Cycles on iOS (ARC)
ARC counts strong references. If A holds B, and B holds A — neither reaches zero count and never gets released. Most frequent patterns:
Closure without [weak self]:
// LEAK
viewModel.onDataLoaded = {
self.tableView.reloadData() // strong reference to self
}
// CORRECT
viewModel.onDataLoaded = { [weak self] in
self?.tableView.reloadData()
}
Timer:
// LEAK — Timer holds target strongly
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true)
When ViewController is closed, timer continues working and holds ViewController. Solution — Timer.scheduledTimer(withTimeInterval:repeats:block:) with [weak self] in block, and mandatory timer.invalidate() in deinit.
Delegate without weak:
// LEAK
protocol DataDelegate: AnyObject { func didLoad() }
class DataService {
var delegate: DataDelegate? // must be weak!
}
If DataService lives longer than delegate or both hold each other — leak. weak var delegate: DataDelegate? — mandatory.
Leaks on Android
Context leak — most common:
// LEAK — Activity Context in singleton
object AppRepository {
private var context: Context? = null
fun init(ctx: Context) { context = ctx } // ctx — Activity
}
Activity is not released while Repository lives. Use only applicationContext in singleton objects.
Anonymous inner class + Handler:
// LEAK — anonymous class implicitly holds reference to Activity
private val handler = Handler(Looper.getMainLooper())
handler.postDelayed({
updateUI() // this — implicit reference to Activity
}, 5000)
Solution: WeakReference<MyActivity> or lifecycleScope.launch { delay(5000); updateUI() } — coroutine is cancelled together with lifecycle.
LiveData observers without removeObserver:
In Fragment we subscribe to ViewModel.liveData.observe(this, ...). If this — not viewLifecycleOwner but Fragment itself — observer lives entire Fragment lifecycle, including periods when View is destroyed. After View recreation — second observer added. After N recreations N observers.
Diagnostics: Tools
LeakCanary (Android) — de facto standard. Add one dependency to debug build, it automatically tracks Activity, Fragment, View, ViewModel. When leak detected — notification with full retain tree. Mandatory in any Android project.
Instruments → Leaks (iOS) — builds object graph and searches for cycles. Run scenario 10–15 times, wait for Leaks to turn red. Click on leak — full object stack with retain-path.
Instruments → Allocations, Generation Analysis — for logical leaks (objects without cyclic references, but that accumulate). Mark generation before action → execute action → see what added and didn't leave.
Android Studio Memory Profiler → Heap Dump — heap snapshot with path to GC root for each object. Look for Activity instances — shouldn't be more than one (active).
Case: RxJava Disposable without Dispose
Flutter developer switched to Android and wrote RxJava code with Observable.interval. Subscription created in onCreate, Disposable nowhere stored. On each screen rotation new Observer created, old continued working. After 10 rotations — 10 active threads. LeakCanary found it in 2 minutes: retained Activity through Observable → Observer → Activity reference.
Solution: CompositeDisposable, add all subscriptions, call disposables.clear() in onStop() or onDestroy().
What We Do Within Service
- Add LeakCanary to Android debug build, configure test scenario runs
- Conduct Instruments Leaks + Allocations sessions for iOS on all key screens
- Analyze all retain-paths of found leaks
- Fix: weak references, timer invalidation, proper closure capture, observer lifecycle
- Add
deinit/onDestroylogging for regression control
Timeframe
Memory leak diagnostics — 1–3 days. Fixing identified leaks — 2–7 days depending on quantity and severity of issues.







